BLOG-90 Intergrate error tracking with Sentry (#120)
All checks were successful
Frontend CI / build (push) Successful in 1m29s

### Description

There are several environment variables should be set:

- Frontend
  - `PUBLIC_SENTRY_DSN`
  - `SENTRY_AUTH_TOKEN`
- Backend
  - `SENTRY_DSN`

If the dsn isn't set, errors won't be sent to Sentry.

### Package Changes

_No response_

### Screenshots

![image.png](/attachments/22e49f8d-ac01-4d09-8ff0-7ce87b787055)

### Reference

Resolves #90

### Checklist

- [x] A milestone is set
- [x] The related issuse has been linked to this branch

Reviewed-on: #120
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
This commit is contained in:
SquidSpirit 2025-08-06 20:20:47 +08:00 committed by squid
parent 171410e115
commit 71bae3d8ca
45 changed files with 2405 additions and 143 deletions

488
backend/Cargo.lock generated
View File

@ -424,9 +424,11 @@ version = "0.2.0"
dependencies = [ dependencies = [
"actix-session", "actix-session",
"actix-web", "actix-web",
"anyhow",
"async-trait", "async-trait",
"log", "common",
"openidconnect", "openidconnect",
"sentry",
"serde", "serde",
"sqlx", "sqlx",
"utoipa", "utoipa",
@ -617,6 +619,13 @@ dependencies = [
"tokio-util", "tokio-util",
] ]
[[package]]
name = "common"
version = "0.2.0"
dependencies = [
"sqlx",
]
[[package]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
version = "2.5.0" version = "2.5.0"
@ -656,6 +665,16 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.7" version = "0.8.7"
@ -804,6 +823,16 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "debugid"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
dependencies = [
"serde",
"uuid",
]
[[package]] [[package]]
name = "der" name = "der"
version = "0.7.10" version = "0.7.10"
@ -1081,6 +1110,18 @@ version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "findshlibs"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64"
dependencies = [
"cc",
"lazy_static",
"libc",
"winapi",
]
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.1.1" version = "1.1.1"
@ -1114,6 +1155,21 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.2.1" version = "1.2.1"
@ -1372,6 +1428,17 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "hostname"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65"
dependencies = [
"cfg-if",
"libc",
"windows-link",
]
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.12" version = "0.2.12"
@ -1465,6 +1532,22 @@ dependencies = [
"webpki-roots 1.0.2", "webpki-roots 1.0.2",
] ]
[[package]]
name = "hyper-tls"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper",
"hyper-util",
"native-tls",
"tokio",
"tokio-native-tls",
"tower-service",
]
[[package]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.16" version = "0.1.16"
@ -1664,10 +1747,12 @@ version = "0.2.0"
dependencies = [ dependencies = [
"actix-multipart", "actix-multipart",
"actix-web", "actix-web",
"anyhow",
"async-trait", "async-trait",
"auth", "auth",
"common",
"futures", "futures",
"log", "sentry",
"serde", "serde",
"sqlx", "sqlx",
"utoipa", "utoipa",
@ -1922,6 +2007,23 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "native-tls"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]] [[package]]
name = "num-bigint" name = "num-bigint"
version = "0.4.6" version = "0.4.6"
@ -2057,6 +2159,50 @@ dependencies = [
"url", "url",
] ]
[[package]]
name = "openssl"
version = "0.10.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-sys"
version = "0.9.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "ordered-float" name = "ordered-float"
version = "2.10.1" version = "2.10.1"
@ -2066,6 +2212,18 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "os_info"
version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0e1ac5fde8d43c34139135df8ea9ee9465394b2d8d20f032d38998f64afffc3"
dependencies = [
"log",
"plist",
"serde",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "p256" name = "p256"
version = "0.13.2" version = "0.13.2"
@ -2199,6 +2357,19 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "plist"
version = "1.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1"
dependencies = [
"base64 0.22.1",
"indexmap 2.9.0",
"quick-xml",
"serde",
"time",
]
[[package]] [[package]]
name = "polyval" name = "polyval"
version = "0.6.2" version = "0.6.2"
@ -2231,10 +2402,12 @@ name = "post"
version = "0.2.0" version = "0.2.0"
dependencies = [ dependencies = [
"actix-web", "actix-web",
"anyhow",
"async-trait", "async-trait",
"auth", "auth",
"chrono", "chrono",
"log", "common",
"sentry",
"serde", "serde",
"sqlx", "sqlx",
"utoipa", "utoipa",
@ -2273,6 +2446,15 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "quick-xml"
version = "0.38.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9845d9dccf565065824e69f9f235fafba1587031eda353c1f1561cd6a6be78f4"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "quinn" name = "quinn"
version = "0.11.8" version = "0.11.8"
@ -2505,9 +2687,11 @@ dependencies = [
"http-body-util", "http-body-util",
"hyper", "hyper",
"hyper-rustls", "hyper-rustls",
"hyper-tls",
"hyper-util", "hyper-util",
"js-sys", "js-sys",
"log", "log",
"native-tls",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"quinn", "quinn",
@ -2518,6 +2702,7 @@ dependencies = [
"serde_urlencoded", "serde_urlencoded",
"sync_wrapper", "sync_wrapper",
"tokio", "tokio",
"tokio-native-tls",
"tokio-rustls", "tokio-rustls",
"tower", "tower",
"tower-http", "tower-http",
@ -2621,6 +2806,15 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [
"rustls-pki-types",
]
[[package]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.11.0" version = "1.11.0"
@ -2653,6 +2847,15 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "schannel"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
dependencies = [
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "schemars" name = "schemars"
version = "0.9.0" version = "0.9.0"
@ -2697,12 +2900,168 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.26" version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
[[package]]
name = "sentry"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "989425268ab5c011e06400187eed6c298272f8ef913e49fcadc3fda788b45030"
dependencies = [
"httpdate",
"native-tls",
"reqwest",
"sentry-actix",
"sentry-anyhow",
"sentry-backtrace",
"sentry-contexts",
"sentry-core",
"sentry-debug-images",
"sentry-panic",
"sentry-tracing",
"tokio",
"ureq",
]
[[package]]
name = "sentry-actix"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5c675bdf6118764a8e265c3395c311b4d905d12866c92df52870c0223d2ffc1"
dependencies = [
"actix-http",
"actix-web",
"bytes",
"futures-util",
"sentry-core",
]
[[package]]
name = "sentry-anyhow"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1b4523c2595d6730bfbe401e95a6423fe9cb16dc3b6046f340551591cffe723"
dependencies = [
"anyhow",
"sentry-backtrace",
"sentry-core",
]
[[package]]
name = "sentry-backtrace"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68e299dd3f7bcf676875eee852c9941e1d08278a743c32ca528e2debf846a653"
dependencies = [
"backtrace",
"regex",
"sentry-core",
]
[[package]]
name = "sentry-contexts"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fac0c5d6892cd4c414492fc957477b620026fb3411fca9fa12774831da561c88"
dependencies = [
"hostname",
"libc",
"os_info",
"rustc_version",
"sentry-core",
"uname",
]
[[package]]
name = "sentry-core"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "deaa38b94e70820ff3f1f9db3c8b0aef053b667be130f618e615e0ff2492cbcc"
dependencies = [
"rand 0.9.1",
"sentry-types",
"serde",
"serde_json",
"url",
]
[[package]]
name = "sentry-debug-images"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00950648aa0d371c7f57057434ad5671bd4c106390df7e7284739330786a01b6"
dependencies = [
"findshlibs",
"sentry-core",
]
[[package]]
name = "sentry-panic"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b7a23b13c004873de3ce7db86eb0f59fe4adfc655a31f7bbc17fd10bacc9bfe"
dependencies = [
"sentry-backtrace",
"sentry-core",
]
[[package]]
name = "sentry-tracing"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fac841c7050aa73fc2bec8f7d8e9cb1159af0b3095757b99820823f3e54e5080"
dependencies = [
"bitflags",
"sentry-backtrace",
"sentry-core",
"tracing-core",
"tracing-subscriber",
]
[[package]]
name = "sentry-types"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e477f4d4db08ddb4ab553717a8d3a511bc9e81dde0c808c680feacbb8105c412"
dependencies = [
"debugid",
"hex",
"rand 0.9.1",
"serde",
"serde_json",
"thiserror 2.0.12",
"time",
"url",
"uuid",
]
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.219" version = "1.0.219"
@ -2822,6 +3181,7 @@ dependencies = [
"openidconnect", "openidconnect",
"percent-encoding", "percent-encoding",
"post", "post",
"sentry",
"sqlx", "sqlx",
"utoipa", "utoipa",
"utoipa-redoc", "utoipa-redoc",
@ -3302,6 +3662,16 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]] [[package]]
name = "tokio-retry" name = "tokio-retry"
version = "0.3.0" version = "0.3.0"
@ -3422,6 +3792,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"valuable",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"tracing-core",
] ]
[[package]] [[package]]
@ -3436,6 +3816,15 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
[[package]]
name = "uname"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.18" version = "0.3.18"
@ -3485,6 +3874,36 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "ureq"
version = "3.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f0fde9bc91026e381155f8c67cb354bcd35260b2f4a29bcc84639f762760c39"
dependencies = [
"base64 0.22.1",
"der",
"log",
"native-tls",
"percent-encoding",
"rustls-pemfile",
"rustls-pki-types",
"ureq-proto",
"utf-8",
"webpki-root-certs 0.26.11",
]
[[package]]
name = "ureq-proto"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59db78ad1923f2b1be62b6da81fe80b173605ca0d57f85da2e005382adf693f7"
dependencies = [
"base64 0.22.1",
"http 1.3.1",
"httparse",
"log",
]
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.4" version = "2.5.4"
@ -3497,6 +3916,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]] [[package]]
name = "utf16_iter" name = "utf16_iter"
version = "1.0.5" version = "1.0.5"
@ -3552,6 +3977,23 @@ dependencies = [
"utoipa", "utoipa",
] ]
[[package]]
name = "uuid"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
dependencies = [
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.15" version = "0.2.15"
@ -3685,6 +4127,24 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "webpki-root-certs"
version = "0.26.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e"
dependencies = [
"webpki-root-certs 1.0.2",
]
[[package]]
name = "webpki-root-certs"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a"
dependencies = [
"rustls-pki-types",
]
[[package]] [[package]]
name = "webpki-roots" name = "webpki-roots"
version = "0.26.10" version = "0.26.10"
@ -3713,6 +4173,28 @@ dependencies = [
"wasite", "wasite",
] ]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.61.0" version = "0.61.0"

View File

@ -1,15 +1,26 @@
[workspace] [workspace]
members = ["server", "feature/auth", "feature/image", "feature/post"] members = [
"server",
"feature/auth",
"feature/common",
"feature/image",
"feature/post",
"feature/common",
]
resolver = "2" resolver = "2"
[workspace.package] [workspace.package]
version = "0.2.0" version = "0.2.0"
edition = "2024" edition = "2024"
[profile.release]
debug = true
[workspace.dependencies] [workspace.dependencies]
actix-multipart = "0.7.2" actix-multipart = "0.7.2"
actix-session = { version = "0.10.1", features = ["redis-session"] } actix-session = { version = "0.10.1", features = ["redis-session"] }
actix-web = "4.10.2" actix-web = "4.10.2"
anyhow = "1.0.98"
async-trait = "0.1.88" async-trait = "0.1.88"
chrono = "0.4.41" chrono = "0.4.41"
dotenv = "0.15.0" dotenv = "0.15.0"
@ -22,6 +33,7 @@ openidconnect = { version = "4.0.1", features = [
"reqwest-blocking", "reqwest-blocking",
] } ] }
percent-encoding = "2.3.1" percent-encoding = "2.3.1"
sentry = { version = "0.42.0", features = ["actix", "anyhow"] }
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
sqlx = { version = "0.8.5", features = [ sqlx = { version = "0.8.5", features = [
"chrono", "chrono",
@ -39,5 +51,6 @@ utoipa-redoc = { version = "6.0.0", features = ["actix-web"] }
server.path = "server" server.path = "server"
auth.path = "feature/auth" auth.path = "feature/auth"
common.path = "feature/common"
image.path = "feature/image" image.path = "feature/image"
post.path = "feature/post" post.path = "feature/post"

View File

@ -13,6 +13,7 @@ COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/server .
EXPOSE 8080 EXPOSE 8080
VOLUME ["/app/static"] VOLUME ["/app/static"]
ENV RUST_LOG=info ENV RUST_LOG=info
ENV RUST_BACKTRACE=1
ENV HOST=0.0.0.0 ENV HOST=0.0.0.0
ENV PORT=8080 ENV PORT=8080
ENV STORAGE_PATH=/app/static ENV STORAGE_PATH=/app/static
@ -27,5 +28,6 @@ ENV OIDC_ISSUER_URL=
ENV OIDC_REDIRECT_URL= ENV OIDC_REDIRECT_URL=
ENV OIDC_CLIENT_ID= ENV OIDC_CLIENT_ID=
ENV OIDC_CLIENT_SECRET= ENV OIDC_CLIENT_SECRET=
ENV SENTRY_DSN=
CMD ["./server"] CMD ["./server"]

View File

@ -6,9 +6,12 @@ edition.workspace = true
[dependencies] [dependencies]
actix-session.workspace = true actix-session.workspace = true
actix-web.workspace = true actix-web.workspace = true
anyhow.workspace = true
async-trait.workspace = true async-trait.workspace = true
log.workspace = true
openidconnect.workspace = true openidconnect.workspace = true
sentry.workspace = true
serde.workspace = true serde.workspace = true
sqlx.workspace = true sqlx.workspace = true
utoipa.workspace = true utoipa.workspace = true
common.workspace = true

View File

@ -1,10 +1,24 @@
#[derive(Debug, PartialEq)] use std::fmt::Display;
#[derive(Debug)]
pub enum AuthError { pub enum AuthError {
DatabaseError(String),
OidcError(String),
InvalidState, InvalidState,
InvalidNonce, InvalidNonce,
InvalidAuthCode, InvalidAuthCode,
InvalidIdToken, InvalidIdToken,
UserNotFound, UserNotFound,
Unexpected(anyhow::Error),
}
impl Display for AuthError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AuthError::InvalidState => write!(f, "Invalid state"),
AuthError::InvalidNonce => write!(f, "Invalid nonce"),
AuthError::InvalidAuthCode => write!(f, "Invalid authentication code"),
AuthError::InvalidIdToken => write!(f, "Invalid ID token"),
AuthError::UserNotFound => write!(f, "User not found"),
AuthError::Unexpected(e) => write!(f, "Unexpected error: {}", e),
}
}
} }

View File

@ -1,4 +1,5 @@
use async_trait::async_trait; use async_trait::async_trait;
use common::framework::error::DatabaseError;
use sqlx::{Pool, Postgres}; use sqlx::{Pool, Postgres};
use crate::{ use crate::{
@ -31,7 +32,7 @@ impl UserDbService for UserDbServiceImpl {
) )
.fetch_optional(&self.db_pool) .fetch_optional(&self.db_pool)
.await .await
.map_err(|e| AuthError::DatabaseError(e.to_string()))?; .map_err(|e| AuthError::Unexpected(DatabaseError(e).into()))?;
match record { match record {
Some(record) => Ok(record.into_mapper()), Some(record) => Ok(record.into_mapper()),
@ -56,7 +57,7 @@ impl UserDbService for UserDbServiceImpl {
) )
.fetch_optional(&self.db_pool) .fetch_optional(&self.db_pool)
.await .await
.map_err(|e| AuthError::DatabaseError(e.to_string()))?; .map_err(|e| AuthError::Unexpected(DatabaseError(e).into()))?;
match record { match record {
Some(record) => Ok(record.into_mapper()), Some(record) => Ok(record.into_mapper()),
@ -78,7 +79,7 @@ impl UserDbService for UserDbServiceImpl {
) )
.fetch_one(&self.db_pool) .fetch_one(&self.db_pool)
.await .await
.map_err(|e| AuthError::DatabaseError(e.to_string()))?; .map_err(|e| AuthError::Unexpected(DatabaseError(e).into()))?;
Ok(id) Ok(id)
} }

View File

@ -80,7 +80,7 @@ impl AuthOidcService for AuthOidcServiceImpl {
let token_response = self let token_response = self
.oidc_client .oidc_client
.exchange_code(AuthorizationCode::new(code.to_string())) .exchange_code(AuthorizationCode::new(code.to_string()))
.map_err(|e| AuthError::OidcError(e.to_string()))? .unwrap()
.request_async(&self.http_client) .request_async(&self.http_client)
.await .await
.map_err(|_| AuthError::InvalidAuthCode)?; .map_err(|_| AuthError::InvalidAuthCode)?;

View File

@ -1,7 +1,10 @@
use std::future::{self, Ready}; use std::{
fmt::Display,
future::{self, Ready},
};
use actix_session::SessionExt; use actix_session::SessionExt;
use actix_web::{Error, FromRequest, HttpRequest, dev::Payload, error::ErrorUnauthorized}; use actix_web::{FromRequest, HttpRequest, dev::Payload};
use crate::framework::web::constants::SESSION_KEY_USER_ID; use crate::framework::web::constants::SESSION_KEY_USER_ID;
@ -14,20 +17,39 @@ impl UserId {
} }
impl FromRequest for UserId { impl FromRequest for UserId {
type Error = Error; type Error = UnauthorizedError;
type Future = Ready<Result<Self, Self::Error>>; type Future = Ready<Result<Self, UnauthorizedError>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
let user_id_result = req.get_session().get::<i32>(SESSION_KEY_USER_ID); let user_id_result = req.get_session().get::<i32>(SESSION_KEY_USER_ID);
let user_id = match user_id_result { let user_id = match user_id_result {
Ok(id) => id, Ok(id) => id,
_ => return future::ready(Err(ErrorUnauthorized(""))), _ => return future::ready(Err(UnauthorizedError)),
}; };
match user_id { match user_id {
Some(id) => future::ready(Ok(UserId(id))), Some(id) => future::ready(Ok(UserId(id))),
None => future::ready(Err(ErrorUnauthorized(""))), None => future::ready(Err(UnauthorizedError)),
} }
} }
} }
#[derive(Debug)]
pub struct UnauthorizedError;
impl Display for UnauthorizedError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Unauthorized access")
}
}
impl actix_web::ResponseError for UnauthorizedError {
fn status_code(&self) -> actix_web::http::StatusCode {
actix_web::http::StatusCode::UNAUTHORIZED
}
fn error_response(&self) -> actix_web::HttpResponse {
actix_web::HttpResponse::Unauthorized().finish()
}
}

View File

@ -1,7 +1,10 @@
use actix_web::{HttpResponse, Responder, web}; use actix_web::{HttpResponse, Responder, web};
use anyhow::anyhow;
use sentry::integrations::anyhow::capture_anyhow;
use crate::{ use crate::{
adapter::delivery::{auth_controller::AuthController, user_response_dto::UserResponseDto}, adapter::delivery::{auth_controller::AuthController, user_response_dto::UserResponseDto},
application::error::auth_error::AuthError,
framework::web::auth_middleware::UserId, framework::web::auth_middleware::UserId,
}; };
@ -26,7 +29,10 @@ pub async fn get_logged_in_user_handler(
match result { match result {
Ok(user) => HttpResponse::Ok().json(user), Ok(user) => HttpResponse::Ok().json(user),
Err(e) => { Err(e) => {
log::error!("{e:?}"); match e {
AuthError::Unexpected(e) => capture_anyhow(&e),
_ => capture_anyhow(&anyhow!(e)),
};
HttpResponse::InternalServerError().finish() HttpResponse::InternalServerError().finish()
} }
} }

View File

@ -1,5 +1,7 @@
use actix_session::Session; use actix_session::Session;
use actix_web::{HttpResponse, Responder, http::header, web}; use actix_web::{HttpResponse, Responder, http::header, web};
use anyhow::anyhow;
use sentry::integrations::anyhow::capture_anyhow;
use crate::{ use crate::{
adapter::delivery::{ adapter::delivery::{
@ -48,7 +50,7 @@ pub async fn oidc_callback_handler(
match result { match result {
Ok(user) => { Ok(user) => {
if let Err(e) = session.insert::<i32>(SESSION_KEY_USER_ID, user.id) { if let Err(e) = session.insert::<i32>(SESSION_KEY_USER_ID, user.id) {
log::error!("{e:?}"); capture_anyhow(&e.into());
return HttpResponse::InternalServerError().finish(); return HttpResponse::InternalServerError().finish();
} }
HttpResponse::Found() HttpResponse::Found()
@ -61,7 +63,10 @@ pub async fn oidc_callback_handler(
| AuthError::InvalidNonce | AuthError::InvalidNonce
| AuthError::InvalidState => HttpResponse::BadRequest().finish(), | AuthError::InvalidState => HttpResponse::BadRequest().finish(),
_ => { _ => {
log::error!("{e:?}"); match e {
AuthError::Unexpected(e) => capture_anyhow(&e),
_ => capture_anyhow(&anyhow!(e)),
};
HttpResponse::InternalServerError().finish() HttpResponse::InternalServerError().finish()
} }
}, },

View File

@ -1,8 +1,11 @@
use actix_session::Session; use actix_session::Session;
use actix_web::{HttpResponse, Responder, http::header, web}; use actix_web::{HttpResponse, Responder, http::header, web};
use anyhow::anyhow;
use sentry::integrations::anyhow::capture_anyhow;
use crate::{ use crate::{
adapter::delivery::auth_controller::AuthController, adapter::delivery::auth_controller::AuthController,
application::error::auth_error::AuthError,
framework::web::constants::{SESSION_KEY_AUTH_NONCE, SESSION_KEY_AUTH_STATE}, framework::web::constants::{SESSION_KEY_AUTH_NONCE, SESSION_KEY_AUTH_STATE},
}; };
@ -24,11 +27,11 @@ pub async fn oidc_login_handler(
match result { match result {
Ok(auth_url) => { Ok(auth_url) => {
if let Err(e) = session.insert::<String>(SESSION_KEY_AUTH_STATE, auth_url.state) { if let Err(e) = session.insert::<String>(SESSION_KEY_AUTH_STATE, auth_url.state) {
log::error!("{e:?}"); capture_anyhow(&e.into());
return HttpResponse::InternalServerError().finish(); return HttpResponse::InternalServerError().finish();
} }
if let Err(e) = session.insert::<String>(SESSION_KEY_AUTH_NONCE, auth_url.nonce) { if let Err(e) = session.insert::<String>(SESSION_KEY_AUTH_NONCE, auth_url.nonce) {
log::error!("{e:?}"); capture_anyhow(&e.into());
return HttpResponse::InternalServerError().finish(); return HttpResponse::InternalServerError().finish();
} }
HttpResponse::Found() HttpResponse::Found()
@ -36,7 +39,10 @@ pub async fn oidc_login_handler(
.finish() .finish()
} }
Err(e) => { Err(e) => {
log::error!("{e:?}"); match e {
AuthError::Unexpected(e) => capture_anyhow(&e),
_ => capture_anyhow(&anyhow!(e)),
};
HttpResponse::InternalServerError().finish() HttpResponse::InternalServerError().finish()
} }
} }

View File

@ -0,0 +1,7 @@
[package]
name = "common"
version.workspace = true
edition.workspace = true
[dependencies]
sqlx.workspace = true

View File

@ -0,0 +1 @@
pub mod error;

View File

@ -0,0 +1,21 @@
use std::fmt::Display;
#[derive(Debug)]
pub struct IOError(pub std::io::Error);
#[derive(Debug)]
pub struct DatabaseError(pub sqlx::Error);
impl std::error::Error for IOError {}
impl Display for IOError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for DatabaseError {}
impl Display for DatabaseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}

View File

@ -0,0 +1 @@
pub mod framework;

View File

@ -6,11 +6,13 @@ edition.workspace = true
[dependencies] [dependencies]
actix-multipart.workspace = true actix-multipart.workspace = true
actix-web.workspace = true actix-web.workspace = true
anyhow.workspace = true
async-trait.workspace = true async-trait.workspace = true
futures.workspace = true futures.workspace = true
log.workspace = true sentry.workspace = true
serde.workspace = true serde.workspace = true
sqlx.workspace = true sqlx.workspace = true
utoipa.workspace = true utoipa.workspace = true
auth.workspace = true auth.workspace = true
common.workspace = true

View File

@ -57,7 +57,7 @@ impl ImageController for ImageControllerImpl {
image: ImageRequestDto, image: ImageRequestDto,
) -> Result<ImageInfoResponseDto, ImageError> { ) -> Result<ImageInfoResponseDto, ImageError> {
if !self.mime_type_whitelist.contains(&image.mime_type) { if !self.mime_type_whitelist.contains(&image.mime_type) {
return Err(ImageError::UnsupportedMimeType); return Err(ImageError::UnsupportedMimeType(image.mime_type));
} }
let mime_type = image.mime_type.clone(); let mime_type = image.mime_type.clone();

View File

@ -1,7 +1,18 @@
#[derive(Debug, PartialEq)] use std::fmt::Display;
#[derive(Debug)]
pub enum ImageError { pub enum ImageError {
DatabaseError(String),
StorageError(String),
NotFound, NotFound,
UnsupportedMimeType, UnsupportedMimeType(String),
Unexpected(anyhow::Error),
}
impl Display for ImageError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ImageError::NotFound => write!(f, "Image not found"),
ImageError::UnsupportedMimeType(mime) => write!(f, "Unsupported MIME type: {}", mime),
ImageError::Unexpected(e) => write!(f, "Unexpected error: {}", e),
}
}
} }

View File

@ -1,4 +1,5 @@
use async_trait::async_trait; use async_trait::async_trait;
use common::framework::error::DatabaseError;
use sqlx::{Pool, Postgres}; use sqlx::{Pool, Postgres};
use crate::{ use crate::{
@ -34,7 +35,7 @@ impl ImageDbService for ImageDbServiceImpl {
match id { match id {
Ok(id) => Ok(id), Ok(id) => Ok(id),
Err(e) => Err(ImageError::DatabaseError(e.to_string())), Err(e) => Err(ImageError::Unexpected(DatabaseError(e).into())),
} }
} }
@ -59,7 +60,7 @@ impl ImageDbService for ImageDbServiceImpl {
}), }),
None => Err(ImageError::NotFound), None => Err(ImageError::NotFound),
}, },
Err(e) => Err(ImageError::DatabaseError(e.to_string())), Err(e) => Err(ImageError::Unexpected(DatabaseError(e).into())),
} }
} }
} }

View File

@ -3,6 +3,8 @@ use std::{
io::Write, io::Write,
}; };
use common::framework::error::IOError;
use crate::{ use crate::{
adapter::gateway::image_storage::ImageStorage, application::error::image_error::ImageError, adapter::gateway::image_storage::ImageStorage, application::error::image_error::ImageError,
}; };
@ -22,20 +24,20 @@ impl ImageStorageImpl {
impl ImageStorage for ImageStorageImpl { impl ImageStorage for ImageStorageImpl {
fn write_data(&self, id: i32, data: &[u8]) -> Result<(), ImageError> { fn write_data(&self, id: i32, data: &[u8]) -> Result<(), ImageError> {
let dir_path = format!("{}/images", self.sotrage_path); let dir_path = format!("{}/images", self.sotrage_path);
fs::create_dir_all(&dir_path).map_err(|e| ImageError::StorageError(e.to_string()))?; fs::create_dir_all(&dir_path).map_err(|e| ImageError::Unexpected(IOError(e).into()))?;
let file_path = format!("{}/{}", dir_path, id); let file_path = format!("{}/{}", dir_path, id);
let mut file = let mut file =
File::create(&file_path).map_err(|e| ImageError::StorageError(e.to_string()))?; File::create(&file_path).map_err(|e| ImageError::Unexpected(IOError(e).into()))?;
file.write_all(data) file.write_all(data)
.map_err(|e| ImageError::StorageError(e.to_string()))?; .map_err(|e| ImageError::Unexpected(e.into()))?;
Ok(()) Ok(())
} }
fn read_data(&self, id: i32) -> Result<Vec<u8>, ImageError> { fn read_data(&self, id: i32) -> Result<Vec<u8>, ImageError> {
let file_path = format!("{}/images/{}", self.sotrage_path, id); let file_path = format!("{}/images/{}", self.sotrage_path, id);
let data = fs::read(&file_path).map_err(|e| ImageError::StorageError(e.to_string()))?; let data = fs::read(&file_path).map_err(|e| ImageError::Unexpected(IOError(e).into()))?;
Ok(data) Ok(data)
} }
} }

View File

@ -1,4 +1,6 @@
use actix_web::{HttpResponse, Responder, web}; use actix_web::{HttpResponse, Responder, web};
use anyhow::anyhow;
use sentry::integrations::anyhow::capture_anyhow;
use utoipa::ToSchema; use utoipa::ToSchema;
use crate::{ use crate::{
@ -29,8 +31,12 @@ pub async fn get_image_by_id_handler(
.body(image_response.data), .body(image_response.data),
Err(e) => match e { Err(e) => match e {
ImageError::NotFound => HttpResponse::NotFound().finish(), ImageError::NotFound => HttpResponse::NotFound().finish(),
ImageError::Unexpected(e) => {
capture_anyhow(&e);
HttpResponse::InternalServerError().finish()
}
_ => { _ => {
log::error!("{e:?}"); capture_anyhow(&anyhow!(e));
HttpResponse::InternalServerError().finish() HttpResponse::InternalServerError().finish()
} }
}, },

View File

@ -1,7 +1,9 @@
use actix_multipart::Multipart; use actix_multipart::Multipart;
use actix_web::{HttpResponse, Responder, web}; use actix_web::{HttpResponse, Responder, web};
use anyhow::anyhow;
use auth::framework::web::auth_middleware::UserId; use auth::framework::web::auth_middleware::UserId;
use futures::StreamExt; use futures::StreamExt;
use sentry::integrations::anyhow::capture_anyhow;
use utoipa::ToSchema; use utoipa::ToSchema;
use crate::{ use crate::{
@ -73,9 +75,15 @@ pub async fn upload_image_handler(
match result { match result {
Ok(image_info) => HttpResponse::Created().json(image_info), Ok(image_info) => HttpResponse::Created().json(image_info),
Err(e) => match e { Err(e) => match e {
ImageError::UnsupportedMimeType => HttpResponse::BadRequest().body(format!("{e:?}")), ImageError::UnsupportedMimeType(mime_type) => {
HttpResponse::BadRequest().body(format!("Unsupported MIME type: {}", mime_type))
}
ImageError::Unexpected(e) => {
capture_anyhow(&e);
HttpResponse::InternalServerError().finish()
}
_ => { _ => {
log::error!("{e:?}"); capture_anyhow(&anyhow!(e));
HttpResponse::InternalServerError().finish() HttpResponse::InternalServerError().finish()
} }
}, },

View File

@ -5,11 +5,13 @@ edition.workspace = true
[dependencies] [dependencies]
actix-web.workspace = true actix-web.workspace = true
anyhow.workspace = true
async-trait.workspace = true async-trait.workspace = true
chrono.workspace = true chrono.workspace = true
log.workspace = true sentry.workspace = true
serde.workspace = true serde.workspace = true
sqlx.workspace = true sqlx.workspace = true
utoipa.workspace = true utoipa.workspace = true
auth.workspace = true auth.workspace = true
common.workspace = true

View File

@ -1,5 +1,16 @@
#[derive(Debug, PartialEq)] use std::fmt::Display;
#[derive(Debug)]
pub enum PostError { pub enum PostError {
DatabaseError(String),
NotFound, NotFound,
Unexpected(anyhow::Error),
}
impl Display for PostError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PostError::NotFound => write!(f, "Post not found"),
PostError::Unexpected(e) => write!(f, "Unexpected error: {}", e),
}
}
} }

View File

@ -1,4 +1,5 @@
use async_trait::async_trait; use async_trait::async_trait;
use common::framework::error::DatabaseError;
use sqlx::{Pool, Postgres}; use sqlx::{Pool, Postgres};
use crate::{ use crate::{
@ -31,7 +32,7 @@ impl LabelDbService for LabelDbServiceImpl {
) )
.fetch_one(&self.db_pool) .fetch_one(&self.db_pool)
.await .await
.map_err(|err| PostError::DatabaseError(err.to_string()))?; .map_err(|e| PostError::Unexpected(DatabaseError(e).into()))?;
Ok(id) Ok(id)
} }
@ -49,7 +50,7 @@ impl LabelDbService for LabelDbServiceImpl {
) )
.execute(&self.db_pool) .execute(&self.db_pool)
.await .await
.map_err(|err| PostError::DatabaseError(err.to_string()))? .map_err(|e| PostError::Unexpected(DatabaseError(e).into()))?
.rows_affected(); .rows_affected();
if affected_rows == 0 { if affected_rows == 0 {
@ -71,7 +72,7 @@ impl LabelDbService for LabelDbServiceImpl {
) )
.fetch_optional(&self.db_pool) .fetch_optional(&self.db_pool)
.await .await
.map_err(|err| PostError::DatabaseError(err.to_string()))?; .map_err(|e| PostError::Unexpected(DatabaseError(e).into()))?;
match record { match record {
Some(record) => Ok(record.into_mapper()), Some(record) => Ok(record.into_mapper()),
@ -91,7 +92,7 @@ impl LabelDbService for LabelDbServiceImpl {
) )
.fetch_all(&self.db_pool) .fetch_all(&self.db_pool)
.await .await
.map_err(|err| PostError::DatabaseError(err.to_string()))?; .map_err(|e| PostError::Unexpected(DatabaseError(e).into()))?;
let mappers = records let mappers = records
.into_iter() .into_iter()

View File

@ -1,6 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use async_trait::async_trait; use async_trait::async_trait;
use common::framework::error::DatabaseError;
use sqlx::{Pool, Postgres}; use sqlx::{Pool, Postgres};
use crate::{ use crate::{
@ -64,7 +65,7 @@ impl PostDbService for PostDbServiceImpl {
.build_query_as::<PostInfoWithLabelRecord>() .build_query_as::<PostInfoWithLabelRecord>()
.fetch_all(&self.db_pool) .fetch_all(&self.db_pool)
.await .await
.map_err(|err| PostError::DatabaseError(err.to_string()))?; .map_err(|e| PostError::Unexpected(DatabaseError(e).into()))?;
let mut post_info_mappers_map = HashMap::<i32, PostInfoMapper>::new(); let mut post_info_mappers_map = HashMap::<i32, PostInfoMapper>::new();
@ -136,7 +137,7 @@ impl PostDbService for PostDbServiceImpl {
.build_query_as::<PostWithLabelRecord>() .build_query_as::<PostWithLabelRecord>()
.fetch_all(&self.db_pool) .fetch_all(&self.db_pool)
.await .await
.map_err(|err| PostError::DatabaseError(err.to_string()))?; .map_err(|e| PostError::Unexpected(DatabaseError(e).into()))?;
if records.is_empty() { if records.is_empty() {
return Err(PostError::NotFound); return Err(PostError::NotFound);
@ -188,7 +189,7 @@ impl PostDbService for PostDbServiceImpl {
.db_pool .db_pool
.begin() .begin()
.await .await
.map_err(|err| PostError::DatabaseError(err.to_string()))?; .map_err(|e| PostError::Unexpected(DatabaseError(e).into()))?;
let post_id = sqlx::query_scalar!( let post_id = sqlx::query_scalar!(
r#" r#"
@ -205,7 +206,7 @@ impl PostDbService for PostDbServiceImpl {
) )
.fetch_one(&mut *tx) .fetch_one(&mut *tx)
.await .await
.map_err(|err| PostError::DatabaseError(err.to_string()))?; .map_err(|e| PostError::Unexpected(DatabaseError(e).into()))?;
for (order, &label_id) in label_ids.iter().enumerate() { for (order, &label_id) in label_ids.iter().enumerate() {
sqlx::query!( sqlx::query!(
@ -221,12 +222,12 @@ impl PostDbService for PostDbServiceImpl {
) )
.execute(&mut *tx) .execute(&mut *tx)
.await .await
.map_err(|err| PostError::DatabaseError(err.to_string()))?; .map_err(|e| PostError::Unexpected(DatabaseError(e).into()))?;
} }
tx.commit() tx.commit()
.await .await
.map_err(|err| PostError::DatabaseError(err.to_string()))?; .map_err(|e| PostError::Unexpected(DatabaseError(e).into()))?;
Ok(post_id) Ok(post_id)
} }
@ -236,7 +237,7 @@ impl PostDbService for PostDbServiceImpl {
.db_pool .db_pool
.begin() .begin()
.await .await
.map_err(|err| PostError::DatabaseError(err.to_string()))?; .map_err(|e| PostError::Unexpected(DatabaseError(e).into()))?;
let affected_rows = sqlx::query!( let affected_rows = sqlx::query!(
r#" r#"
@ -258,7 +259,7 @@ impl PostDbService for PostDbServiceImpl {
) )
.execute(&mut *tx) .execute(&mut *tx)
.await .await
.map_err(|err| PostError::DatabaseError(err.to_string()))? .map_err(|e| PostError::Unexpected(DatabaseError(e).into()))?
.rows_affected(); .rows_affected();
if affected_rows == 0 { if affected_rows == 0 {
@ -274,7 +275,7 @@ impl PostDbService for PostDbServiceImpl {
) )
.execute(&mut *tx) .execute(&mut *tx)
.await .await
.map_err(|err| PostError::DatabaseError(err.to_string()))?; .map_err(|e| PostError::Unexpected(DatabaseError(e).into()))?;
for (order, &label_id) in label_ids.iter().enumerate() { for (order, &label_id) in label_ids.iter().enumerate() {
sqlx::query!( sqlx::query!(
@ -290,12 +291,12 @@ impl PostDbService for PostDbServiceImpl {
) )
.execute(&mut *tx) .execute(&mut *tx)
.await .await
.map_err(|err| PostError::DatabaseError(err.to_string()))?; .map_err(|e| PostError::Unexpected(DatabaseError(e).into()))?;
} }
tx.commit() tx.commit()
.await .await
.map_err(|err| PostError::DatabaseError(err.to_string()))?; .map_err(|e| PostError::Unexpected(DatabaseError(e).into()))?;
Ok(()) Ok(())
} }

View File

@ -1,9 +1,14 @@
use actix_web::{HttpResponse, Responder, web}; use actix_web::{HttpResponse, Responder, web};
use anyhow::anyhow;
use auth::framework::web::auth_middleware::UserId; use auth::framework::web::auth_middleware::UserId;
use sentry::integrations::anyhow::capture_anyhow;
use crate::adapter::delivery::{ use crate::{
create_label_request_dto::CreateLabelRequestDto, label_response_dto::LabelResponseDto, adapter::delivery::{
post_controller::PostController, create_label_request_dto::CreateLabelRequestDto, label_response_dto::LabelResponseDto,
post_controller::PostController,
},
application::error::post_error::PostError,
}; };
#[utoipa::path( #[utoipa::path(
@ -28,7 +33,10 @@ pub async fn create_label_handler(
match result { match result {
Ok(label) => HttpResponse::Created().json(label), Ok(label) => HttpResponse::Created().json(label),
Err(e) => { Err(e) => {
log::error!("{e:?}"); match e {
PostError::Unexpected(e) => capture_anyhow(&e),
_ => capture_anyhow(&anyhow!(e)),
};
HttpResponse::InternalServerError().finish() HttpResponse::InternalServerError().finish()
} }
} }

View File

@ -1,9 +1,14 @@
use actix_web::{web, HttpResponse, Responder}; use actix_web::{HttpResponse, Responder, web};
use anyhow::anyhow;
use auth::framework::web::auth_middleware::UserId; use auth::framework::web::auth_middleware::UserId;
use sentry::integrations::anyhow::capture_anyhow;
use crate::adapter::delivery::{ use crate::{
create_post_request_dto::CreatePostRequestDto, post_controller::PostController, adapter::delivery::{
post_response_dto::PostResponseDto, create_post_request_dto::CreatePostRequestDto, post_controller::PostController,
post_response_dto::PostResponseDto,
},
application::error::post_error::PostError,
}; };
#[utoipa::path( #[utoipa::path(
@ -28,7 +33,10 @@ pub async fn create_post_handler(
match result { match result {
Ok(post) => HttpResponse::Created().json(post), Ok(post) => HttpResponse::Created().json(post),
Err(e) => { Err(e) => {
log::error!("{e:?}"); match e {
PostError::Unexpected(e) => capture_anyhow(&e),
_ => capture_anyhow(&anyhow!(e)),
};
HttpResponse::InternalServerError().finish() HttpResponse::InternalServerError().finish()
} }
} }

View File

@ -1,7 +1,10 @@
use actix_web::{HttpResponse, Responder, web}; use actix_web::{HttpResponse, Responder, web};
use anyhow::anyhow;
use sentry::integrations::anyhow::capture_anyhow;
use crate::adapter::delivery::{ use crate::{
label_response_dto::LabelResponseDto, post_controller::PostController, adapter::delivery::{label_response_dto::LabelResponseDto, post_controller::PostController},
application::error::post_error::PostError,
}; };
#[utoipa::path( #[utoipa::path(
@ -21,7 +24,10 @@ pub async fn get_all_labels_handler(
match result { match result {
Ok(labels) => HttpResponse::Ok().json(labels), Ok(labels) => HttpResponse::Ok().json(labels),
Err(e) => { Err(e) => {
log::error!("{e:?}"); match e {
PostError::Unexpected(e) => capture_anyhow(&e),
_ => capture_anyhow(&anyhow!(e)),
};
HttpResponse::InternalServerError().finish() HttpResponse::InternalServerError().finish()
} }
} }

View File

@ -1,8 +1,13 @@
use actix_web::{HttpResponse, Responder, web}; use actix_web::{HttpResponse, Responder, web};
use anyhow::anyhow;
use sentry::integrations::anyhow::capture_anyhow;
use crate::adapter::delivery::{ use crate::{
post_controller::PostController, post_info_query_dto::PostQueryDto, adapter::delivery::{
post_info_response_dto::PostInfoResponseDto, post_controller::PostController, post_info_query_dto::PostQueryDto,
post_info_response_dto::PostInfoResponseDto,
},
application::error::post_error::PostError,
}; };
#[utoipa::path( #[utoipa::path(
@ -26,7 +31,10 @@ pub async fn get_all_post_info_handler(
match result { match result {
Ok(post_info_list) => HttpResponse::Ok().json(post_info_list), Ok(post_info_list) => HttpResponse::Ok().json(post_info_list),
Err(e) => { Err(e) => {
log::error!("{e:?}"); match e {
PostError::Unexpected(e) => capture_anyhow(&e),
_ => capture_anyhow(&anyhow!(e)),
};
HttpResponse::InternalServerError().finish() HttpResponse::InternalServerError().finish()
} }
} }

View File

@ -1,4 +1,5 @@
use actix_web::{HttpResponse, Responder, web}; use actix_web::{HttpResponse, Responder, web};
use sentry::integrations::anyhow::capture_anyhow;
use crate::{ use crate::{
adapter::delivery::{post_controller::PostController, post_response_dto::PostResponseDto}, adapter::delivery::{post_controller::PostController, post_response_dto::PostResponseDto},
@ -24,13 +25,12 @@ pub async fn get_post_by_id_handler(
match result { match result {
Ok(post) => HttpResponse::Ok().json(post), Ok(post) => HttpResponse::Ok().json(post),
Err(e) => { Err(e) => match e {
if e == PostError::NotFound { PostError::NotFound => HttpResponse::NotFound().finish(),
HttpResponse::NotFound().finish() PostError::Unexpected(e) => {
} else { capture_anyhow(&e);
log::error!("{e:?}");
HttpResponse::InternalServerError().finish() HttpResponse::InternalServerError().finish()
} }
} },
} }
} }

View File

@ -1,5 +1,6 @@
use actix_web::{HttpResponse, Responder, web}; use actix_web::{HttpResponse, Responder, web};
use auth::framework::web::auth_middleware::UserId; use auth::framework::web::auth_middleware::UserId;
use sentry::integrations::anyhow::capture_anyhow;
use crate::{ use crate::{
adapter::delivery::{ adapter::delivery::{
@ -37,8 +38,8 @@ pub async fn update_label_handler(
Ok(label) => HttpResponse::Ok().json(label), Ok(label) => HttpResponse::Ok().json(label),
Err(e) => match e { Err(e) => match e {
PostError::NotFound => HttpResponse::NotFound().finish(), PostError::NotFound => HttpResponse::NotFound().finish(),
_ => { PostError::Unexpected(e) => {
log::error!("{e:?}"); capture_anyhow(&e);
HttpResponse::InternalServerError().finish() HttpResponse::InternalServerError().finish()
} }
}, },

View File

@ -1,9 +1,13 @@
use actix_web::{HttpResponse, Responder, web}; use actix_web::{HttpResponse, Responder, web};
use auth::framework::web::auth_middleware::UserId; use auth::framework::web::auth_middleware::UserId;
use sentry::integrations::anyhow::capture_anyhow;
use crate::adapter::delivery::{ use crate::{
post_controller::PostController, post_response_dto::PostResponseDto, adapter::delivery::{
update_post_request_dto::UpdatePostRequestDto, post_controller::PostController, post_response_dto::PostResponseDto,
update_post_request_dto::UpdatePostRequestDto,
},
application::error::post_error::PostError,
}; };
#[utoipa::path( #[utoipa::path(
@ -30,14 +34,12 @@ pub async fn update_post_handler(
match result { match result {
Ok(post) => HttpResponse::Ok().json(post), Ok(post) => HttpResponse::Ok().json(post),
Err(e) => { Err(e) => match e {
log::error!("{e:?}"); PostError::NotFound => HttpResponse::NotFound().finish(),
match e { PostError::Unexpected(e) => {
crate::application::error::post_error::PostError::NotFound => { capture_anyhow(&e);
HttpResponse::NotFound().finish() HttpResponse::InternalServerError().finish()
}
_ => HttpResponse::InternalServerError().finish(),
} }
} },
} }
} }

View File

@ -11,6 +11,7 @@ env_logger.workspace = true
hex.workspace = true hex.workspace = true
openidconnect.workspace = true openidconnect.workspace = true
percent-encoding.workspace = true percent-encoding.workspace = true
sentry.workspace = true
sqlx.workspace = true sqlx.workspace = true
utoipa.workspace = true utoipa.workspace = true
utoipa-redoc.workspace = true utoipa-redoc.workspace = true

View File

@ -1,12 +1,13 @@
use openidconnect::reqwest; use openidconnect::reqwest;
use crate::configuration::{ use crate::configuration::{
db::DbConfiguration, oidc::OidcConfiguration, server::ServerConfiguration, db::DbConfiguration, oidc::OidcConfiguration, sentry::SentryConfiguration,
session::SessionConfiguration, storage::StorageConfiguration, server::ServerConfiguration, session::SessionConfiguration, storage::StorageConfiguration,
}; };
pub mod db; pub mod db;
pub mod oidc; pub mod oidc;
pub mod sentry;
pub mod server; pub mod server;
pub mod session; pub mod session;
pub mod storage; pub mod storage;
@ -15,6 +16,7 @@ pub mod storage;
pub struct Configuration { pub struct Configuration {
pub db: DbConfiguration, pub db: DbConfiguration,
pub oidc: OidcConfiguration, pub oidc: OidcConfiguration,
pub sentry: SentryConfiguration,
pub server: ServerConfiguration, pub server: ServerConfiguration,
pub session: SessionConfiguration, pub session: SessionConfiguration,
pub storage: StorageConfiguration, pub storage: StorageConfiguration,
@ -25,6 +27,7 @@ impl Configuration {
Self { Self {
db: DbConfiguration::new(), db: DbConfiguration::new(),
oidc: OidcConfiguration::new(http_client).await, oidc: OidcConfiguration::new(http_client).await,
sentry: SentryConfiguration::new(),
server: ServerConfiguration::new(), server: ServerConfiguration::new(),
session: SessionConfiguration::new(), session: SessionConfiguration::new(),
storage: StorageConfiguration::new(), storage: StorageConfiguration::new(),

View File

@ -0,0 +1,23 @@
#[derive(Clone)]
pub struct SentryConfiguration {
pub dsn: String,
pub options: sentry::ClientOptions,
}
impl SentryConfiguration {
pub fn new() -> Self {
let dsn = std::env::var("SENTRY_DSN").unwrap_or("".to_string());
Self {
dsn: dsn,
options: sentry::ClientOptions {
release: sentry::release_name!(),
traces_sample_rate: 1.0,
send_default_pii: true,
max_request_body_size: sentry::MaxRequestBodySize::Always,
attach_stacktrace: true,
..Default::default()
},
}
}
}

View File

@ -5,17 +5,19 @@ use actix_web::{
App, Error, HttpServer, App, Error, HttpServer,
body::MessageBody, body::MessageBody,
dev::{ServiceFactory, ServiceRequest, ServiceResponse}, dev::{ServiceFactory, ServiceRequest, ServiceResponse},
rt::Runtime,
web, web,
}; };
use auth::framework::web::auth_web_routes::configure_auth_routes; use auth::framework::web::auth_web_routes::configure_auth_routes;
use image::framework::web::image_web_routes::configure_image_routes; use image::framework::web::image_web_routes::configure_image_routes;
use openidconnect::reqwest; use openidconnect::reqwest;
use post::framework::web::post_web_routes::configure_post_routes; use post::framework::web::post_web_routes::configure_post_routes;
use server::{api_doc::configure_api_doc_routes, configuration::Configuration, container::Container}; use server::{
api_doc::configure_api_doc_routes, configuration::Configuration, container::Container,
};
use sqlx::{Pool, Postgres}; use sqlx::{Pool, Postgres};
#[actix_web::main] fn main() -> std::io::Result<()> {
async fn main() -> std::io::Result<()> {
dotenv::dotenv().ok(); dotenv::dotenv().ok();
env_logger::init(); env_logger::init();
@ -24,31 +26,45 @@ async fn main() -> std::io::Result<()> {
.build() .build()
.expect("Failed to create HTTP client"); .expect("Failed to create HTTP client");
let configuration = Configuration::new(http_client.clone()).await; let rt = Runtime::new().unwrap();
let configuration = rt.block_on(async { Configuration::new(http_client.clone()).await });
let host = configuration.server.host.clone(); let _guard = sentry::init((
let port = configuration.server.port; configuration.sentry.dsn.clone(),
configuration.sentry.options.clone(),
));
let db_pool = configuration.db.create_connection().await; actix_web::rt::System::new().block_on(async {
let session_key = configuration.session.session_key.clone(); let host = configuration.server.host.clone();
let session_store = configuration.session.create_session_store().await; let port = configuration.server.port;
HttpServer::new(move || { let db_pool = configuration.db.create_connection().await;
create_app( let session_key = configuration.session.session_key.clone();
db_pool.clone(), let session_store = configuration.session.create_session_store().await;
http_client.clone(),
SessionMiddleware::builder(session_store.clone(), session_key.clone()), HttpServer::new(move || {
configuration.clone(), create_app(
) db_pool.clone(),
}) http_client.clone(),
.bind((host, port))? sentry::integrations::actix::Sentry::builder()
.run() .capture_server_errors(true)
.await .start_transaction(true),
SessionMiddleware::builder(session_store.clone(), session_key.clone()),
configuration.clone(),
)
})
.bind((host, port))?
.run()
.await
})?;
Ok(())
} }
fn create_app( fn create_app(
db_pool: Pool<Postgres>, db_pool: Pool<Postgres>,
http_client: reqwest::Client, http_client: reqwest::Client,
sentry_builder: sentry::integrations::actix::SentryBuilder,
session_middleware_builder: SessionMiddlewareBuilder<RedisSessionStore>, session_middleware_builder: SessionMiddlewareBuilder<RedisSessionStore>,
configuration: Configuration, configuration: Configuration,
) -> App< ) -> App<
@ -64,6 +80,7 @@ fn create_app(
App::new() App::new()
// The middlewares are executed in opposite order as registration. // The middlewares are executed in opposite order as registration.
.wrap(sentry_builder.finish())
.wrap(session_middleware_builder.build()) .wrap(session_middleware_builder.build())
.app_data(web::Data::from(container.auth_controller)) .app_data(web::Data::from(container.auth_controller))
.app_data(web::Data::from(container.image_controller)) .app_data(web::Data::from(container.image_controller))

3
frontend/.gitignore vendored
View File

@ -21,3 +21,6 @@ Thumbs.db
# Vite # Vite
vite.config.js.timestamp-* vite.config.js.timestamp-*
vite.config.ts.timestamp-* vite.config.ts.timestamp-*
# Sentry Config File
.env.sentry-build-plugin

View File

@ -23,6 +23,8 @@ EXPOSE 3000
ENV NODE_ENV=production ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0 ENV HOSTNAME=0.0.0.0
ENV PORT=3000 ENV PORT=3000
ENV SENTRY_AUTH_TOKEN=
ENV PUBLIC_SENTRY_DSN=
ENV PUBLIC_API_BASE_URL=http://127.0.0.1:8080/ ENV PUBLIC_API_BASE_URL=http://127.0.0.1:8080/
ENV PUBLIC_GA_MEASUREMENT_ID= ENV PUBLIC_GA_MEASUREMENT_ID=
CMD ["node", "build"] CMD ["node", "build"]

View File

@ -47,5 +47,8 @@
"esbuild" "esbuild"
] ]
}, },
"packageManager": "pnpm@10.12.4" "packageManager": "pnpm@10.12.4",
"dependencies": {
"@sentry/sveltekit": "^10.1.0"
}
} }

1550
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
import { Environment } from '$lib/environment';
import { handleErrorWithSentry, replayIntegration } from '@sentry/sveltekit';
import * as Sentry from '@sentry/sveltekit';
Sentry.init({
dsn: Environment.SENTRY_DSN,
tracesSampleRate: 1.0,
// Enable logs to be sent to Sentry
enableLogs: true,
// This sets the sample rate to be 10%. You may want this to be 100% while
// in development and sample at a lower rate in production
replaysSessionSampleRate: 0.1,
// If the entire session is not sampled, use the below sample rate to sample
// sessions when an error occurs.
replaysOnErrorSampleRate: 1.0,
// If you don't want to use Session Replay, just remove the line below:
integrations: [replayIntegration()]
});
// If you have a custom error handler, pass it to `handleErrorWithSentry`
export const handleError = handleErrorWithSentry();

View File

@ -1,3 +1,5 @@
import { sequence } from '@sveltejs/kit/hooks';
import * as Sentry from '@sentry/sveltekit';
import { PostRepositoryImpl } from '$lib/post/adapter/gateway/postRepositoryImpl'; import { PostRepositoryImpl } from '$lib/post/adapter/gateway/postRepositoryImpl';
import { PostBloc } from '$lib/post/adapter/presenter/postBloc'; import { PostBloc } from '$lib/post/adapter/presenter/postBloc';
import { PostListBloc } from '$lib/post/adapter/presenter/postListBloc'; import { PostListBloc } from '$lib/post/adapter/presenter/postListBloc';
@ -5,8 +7,15 @@ import { GetAllPostsUseCase } from '$lib/post/application/useCase/getAllPostsUse
import { GetPostUseCase } from '$lib/post/application/useCase/getPostUseCase'; import { GetPostUseCase } from '$lib/post/application/useCase/getPostUseCase';
import { PostApiServiceImpl } from '$lib/post/framework/api/postApiServiceImpl'; import { PostApiServiceImpl } from '$lib/post/framework/api/postApiServiceImpl';
import type { Handle } from '@sveltejs/kit'; import type { Handle } from '@sveltejs/kit';
import { Environment } from '$lib/environment';
export const handle: Handle = ({ event, resolve }) => { Sentry.init({
dsn: Environment.SENTRY_DSN,
tracesSampleRate: 1,
enableLogs: true
});
export const handle: Handle = sequence(Sentry.sentryHandle(), ({ event, resolve }) => {
const postApiService = new PostApiServiceImpl(event.fetch); const postApiService = new PostApiServiceImpl(event.fetch);
const postRepository = new PostRepositoryImpl(postApiService); const postRepository = new PostRepositoryImpl(postApiService);
const getAllPostsUseCase = new GetAllPostsUseCase(postRepository); const getAllPostsUseCase = new GetAllPostsUseCase(postRepository);
@ -16,4 +25,6 @@ export const handle: Handle = ({ event, resolve }) => {
event.locals.postBloc = new PostBloc(getPostUseCase); event.locals.postBloc = new PostBloc(getPostUseCase);
return resolve(event); return resolve(event);
}; });
export const handleError = Sentry.handleErrorWithSentry();

View File

@ -3,4 +3,5 @@ import { env } from '$env/dynamic/public';
export abstract class Environment { export abstract class Environment {
static readonly API_BASE_URL = env.PUBLIC_API_BASE_URL ?? 'http://localhost:5173/api/'; static readonly API_BASE_URL = env.PUBLIC_API_BASE_URL ?? 'http://localhost:5173/api/';
static readonly GA_MEASUREMENT_ID = env.PUBLIC_GA_MEASUREMENT_ID ?? ''; static readonly GA_MEASUREMENT_ID = env.PUBLIC_GA_MEASUREMENT_ID ?? '';
static readonly SENTRY_DSN = env.PUBLIC_SENTRY_DSN ?? '';
} }

View File

@ -1,3 +1,4 @@
import { sentrySvelteKit } from '@sentry/sveltekit';
import tailwindcss from '@tailwindcss/vite'; import tailwindcss from '@tailwindcss/vite';
import { sveltekit } from '@sveltejs/kit/vite'; import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
@ -5,7 +6,16 @@ import { defineConfig } from 'vite';
import { version } from './package.json'; import { version } from './package.json';
export default defineConfig({ export default defineConfig({
plugins: [tailwindcss(), sveltekit()], plugins: [
sentrySvelteKit({
sourceMapsUploadOptions: {
org: 'squidspirit',
project: 'blog-beta-frontend'
}
}),
tailwindcss(),
sveltekit()
],
define: { define: {
'App.__VERSION__': JSON.stringify(version) 'App.__VERSION__': JSON.stringify(version)
}, },