Add SECURITY_TRIAGE.md for vulnerability assessment and remediation plan
- Document current state of vulnerabilities in Rust and frontend dependencies - Outline active vulnerabilities and their remediation status - Include notes on resolved issues and remaining bugs - Define a remediation plan with prioritized actions
This commit is contained in:
Generated
+109
-136
@@ -578,7 +578,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper 1.0.2",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
@@ -599,7 +599,7 @@ dependencies = [
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper 1.0.2",
|
||||
"sync_wrapper",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -628,12 +628,6 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
@@ -675,12 +669,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
@@ -775,6 +763,12 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.42"
|
||||
@@ -829,7 +823,7 @@ dependencies = [
|
||||
"openidconnect",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"reqwest 0.12.26",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
@@ -856,7 +850,7 @@ dependencies = [
|
||||
"hmac",
|
||||
"jsonwebtoken",
|
||||
"openidconnect",
|
||||
"reqwest 0.12.26",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
@@ -1856,6 +1850,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.4",
|
||||
"tower-service",
|
||||
"webpki-roots 1.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1893,7 +1888,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.6.1",
|
||||
"system-configuration 0.6.1",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -2183,7 +2178,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags",
|
||||
"libc",
|
||||
"redox_syscall 0.6.0",
|
||||
]
|
||||
@@ -2230,7 +2225,7 @@ dependencies = [
|
||||
"mime_guess",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"reqwest 0.12.26",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
@@ -2271,6 +2266,12 @@ dependencies = [
|
||||
"hashbrown 0.15.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
@@ -2473,16 +2474,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oauth2"
|
||||
version = "4.4.2"
|
||||
version = "5.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f"
|
||||
checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"getrandom 0.2.17",
|
||||
"http 0.2.12",
|
||||
"http 1.4.0",
|
||||
"rand 0.8.5",
|
||||
"reqwest 0.11.27",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
@@ -2499,16 +2500,16 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "openidconnect"
|
||||
version = "3.5.0"
|
||||
version = "4.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f47e80a9cfae4462dd29c41e987edd228971d6565553fbc14b8a11e666d91590"
|
||||
checksum = "0d8c6709ba2ea764bbed26bce1adf3c10517113ddea6f2d4196e4851757ef2b2"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"base64 0.21.7",
|
||||
"chrono",
|
||||
"dyn-clone",
|
||||
"ed25519-dalek",
|
||||
"hmac",
|
||||
"http 0.2.12",
|
||||
"http 1.4.0",
|
||||
"itertools",
|
||||
"log",
|
||||
"oauth2",
|
||||
@@ -2518,7 +2519,6 @@ dependencies = [
|
||||
"rsa",
|
||||
"serde",
|
||||
"serde-value",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_plain",
|
||||
@@ -2535,7 +2535,7 @@ version = "0.10.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
@@ -2838,6 +2838,61 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg_aliases",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls 0.23.35",
|
||||
"socket2 0.6.1",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"getrandom 0.3.4",
|
||||
"lru-slab",
|
||||
"rand 0.9.2",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls 0.23.35",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
"thiserror 2.0.17",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
|
||||
dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2 0.6.1",
|
||||
"tracing",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
@@ -2924,7 +2979,7 @@ version = "11.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2933,7 +2988,7 @@ version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2942,7 +2997,7 @@ version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3000,47 +3055,6 @@ version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2 0.3.27",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"hyper 0.14.32",
|
||||
"hyper-rustls 0.24.2",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls 0.21.12",
|
||||
"rustls-pemfile",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper 0.1.2",
|
||||
"system-configuration 0.5.1",
|
||||
"tokio",
|
||||
"tokio-rustls 0.24.1",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"webpki-roots 0.25.4",
|
||||
"winreg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.26"
|
||||
@@ -3067,13 +3081,16 @@ dependencies = [
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls 0.23.35",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper 1.0.2",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls 0.26.4",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
@@ -3081,6 +3098,7 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"webpki-roots 1.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3138,6 +3156,12 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -3153,7 +3177,7 @@ version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
@@ -3199,21 +3223,13 @@ dependencies = [
|
||||
"security-framework 3.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -3334,7 +3350,7 @@ version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags",
|
||||
"core-foundation 0.9.4",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
@@ -3347,7 +3363,7 @@ version = "3.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
@@ -3740,7 +3756,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.10.0",
|
||||
"bitflags",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"chrono",
|
||||
@@ -3784,7 +3800,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.10.0",
|
||||
"bitflags",
|
||||
"byteorder",
|
||||
"chrono",
|
||||
"crc",
|
||||
@@ -3881,12 +3897,6 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "1.0.2"
|
||||
@@ -3907,36 +3917,15 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys 0.5.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys 0.6.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration-sys"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4158,7 +4147,7 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper 1.0.2",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
@@ -4171,7 +4160,7 @@ version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"bitflags",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
@@ -4527,12 +4516,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.25.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.11"
|
||||
@@ -4875,16 +4858,6 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.50.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ reqwest = { version = "0.12", features = ["json", "multipart"] }
|
||||
hmac = "0.12"
|
||||
sha2 = "0.10"
|
||||
hex = "0.4"
|
||||
openidconnect = { version = "3.5", features = ["reqwest"] }
|
||||
openidconnect = { version = "4", features = ["reqwest"] }
|
||||
anyhow = "1.0"
|
||||
utoipa = { version = "5", features = ["axum_extras", "chrono", "uuid"] }
|
||||
thiserror = "2.0"
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# Security Triage
|
||||
|
||||
Fecha de última actualización: 2026-04-28
|
||||
|
||||
---
|
||||
|
||||
## Estado actual (post-remediación)
|
||||
|
||||
### Rust (cargo audit)
|
||||
- Vulnerabilidades: 4 (todas en dependencias transitivas de terceros sin fix directo)
|
||||
- Warnings: 4
|
||||
|
||||
#### Vulnerabilidades activas
|
||||
|
||||
1. RUSTSEC-2023-0071 (rsa 0.9.9)
|
||||
- Severidad: media (Marvin Attack)
|
||||
- Cadena: sqlx-mysql → rsa
|
||||
- Fix upstream: no disponible
|
||||
- Estado: aceptación de riesgo temporal
|
||||
|
||||
2. RUSTSEC-2026-0098 (rustls-webpki 0.101.7)
|
||||
- Nombre: URI name constraints incorrectly accepted
|
||||
- Cadena: aws-sdk-s3/aws-config → aws-smithy-http-client → rustls 0.21 → rustls-webpki 0.101.7
|
||||
- Fix: requiere AWS SDK 1.x actualice su stack TLS a rustls >=0.22
|
||||
- Estado: bloqueado por tercero (AWS SDK)
|
||||
|
||||
3. RUSTSEC-2026-0099 (rustls-webpki 0.101.7)
|
||||
- Nombre: wildcard name constraints incorrectly accepted
|
||||
- Misma cadena que 0098
|
||||
|
||||
4. RUSTSEC-2026-0104 (rustls-webpki 0.101.7)
|
||||
- Nombre: panic alcanzable en CRL parsing
|
||||
- Misma cadena que 0098
|
||||
|
||||
#### Nota: openidconnect ya NO es un vector
|
||||
- Se actualizó openidconnect de 3.5 → 4.x
|
||||
- openidconnect 4.x usa reqwest 0.12 + rustls-webpki 0.103.13 (parcheado) ✓
|
||||
- Código de handlers SSO actualizado para nueva API (reqwest::Client en lugar de async_http_client)
|
||||
|
||||
---
|
||||
|
||||
### Frontend (npm audit --omit=dev)
|
||||
|
||||
#### Studio
|
||||
- Pre-remediación: 1 critical, 12 high, 1 moderate (total 14)
|
||||
- Post-remediación: 0 critical, 2 high, 3 moderate (total 5)
|
||||
- Resuelto: dompurify critical + toda la cadena d3/mermaid/dagre-d3 (via upgrade mermaid 9 → 11.14.0)
|
||||
- Restante high: xlsx (2 advisories, sin fix disponible)
|
||||
- Restante moderate: next + postcss (requieren Next major upgrade)
|
||||
|
||||
#### Experience
|
||||
- Pre-remediación: 1 critical, 11 high, 1 moderate (total 13)
|
||||
- Post-remediación: 0 critical, 1 high, 3 moderate (total 4)
|
||||
- Resuelto: dompurify critical + toda la cadena d3/mermaid/dagre-d3 ✓
|
||||
- Restante high: next (requiere Next major upgrade)
|
||||
- Restante moderate: next postcss + uuid (moderate, via mermaid 11; revertir a mermaid 9 empeoraría)
|
||||
|
||||
---
|
||||
|
||||
## Bugs corregidos (colaterales al triage)
|
||||
|
||||
- Studio: función PeerReviewDashboard duplicada en peer-reviews/page.tsx (compilación OK)
|
||||
- Experience: PeerReviewPlayer duplicada en blocks/PeerReviewPlayer.tsx (compilación OK)
|
||||
- Experience: try sin catch en handleBlockComplete de lessons/[lessonId]/page.tsx
|
||||
- Backend: variable err_body no usada en handlers LMS (warning eliminado)
|
||||
|
||||
---
|
||||
|
||||
## Plan de remediación restante
|
||||
|
||||
### P1 (pendiente - bloqueado por terceros)
|
||||
- rustls-webpki via AWS SDK: monitorear release del AWS SDK que actualice a rustls 0.22+
|
||||
- Validar con: cargo update && cargo audit
|
||||
- ETA: depende de AWS SDK Rust team
|
||||
|
||||
### P2 (decisión de producto)
|
||||
- xlsx en studio: sin fix disponible en npm
|
||||
- Opción A: encapsular parseo de Excel en backend y eliminar xlsx del frontend
|
||||
- Opción B: documentar excepción + limitar tamaño y tipo de archivos subidos
|
||||
|
||||
### P3 (pendiente)
|
||||
- Next.js major upgrade (14.x → 15.x o superior con fix)
|
||||
- Requiere checklist de breaking changes de Next
|
||||
- Validar: rutas, middleware, auth flows, SSR
|
||||
- Recomendado ejecutar en rama separada con suite E2E completa
|
||||
|
||||
@@ -33,7 +33,6 @@ use reqwest::header::HeaderMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
use openidconnect::core::{CoreClient, CoreProviderMetadata, CoreResponseType};
|
||||
use openidconnect::reqwest::async_http_client;
|
||||
use openidconnect::{
|
||||
AuthenticationFlow, AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce,
|
||||
RedirectUrl, Scope, TokenResponse,
|
||||
@@ -606,7 +605,7 @@ pub async fn update_course(
|
||||
let mut tx = pool
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Establecer contexto de auditoría
|
||||
sqlx::query(
|
||||
@@ -616,7 +615,7 @@ pub async fn update_course(
|
||||
.bind(org_ctx.id.to_string())
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let course = sqlx::query_as::<_, Course>(
|
||||
"SELECT * FROM fn_update_course($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)",
|
||||
@@ -645,7 +644,7 @@ pub async fn update_course(
|
||||
|
||||
tx.commit()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(course))
|
||||
}
|
||||
@@ -1756,7 +1755,7 @@ pub async fn get_grading_categories(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(categories))
|
||||
}
|
||||
@@ -1779,7 +1778,7 @@ pub async fn create_grading_category(
|
||||
.bind(payload.tipo_nota_id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(category))
|
||||
}
|
||||
@@ -1816,7 +1815,7 @@ pub async fn delete_grading_category(
|
||||
.bind(org_ctx.id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
@@ -2793,7 +2792,7 @@ pub async fn register(
|
||||
|
||||
// Validar formato de email (validación básica)
|
||||
let email_regex = regex::Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Email validation error".into()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
if !email_regex.is_match(&payload.email) {
|
||||
return Err((StatusCode::BAD_REQUEST, "Formato de email inválido".into()));
|
||||
}
|
||||
@@ -2817,7 +2816,7 @@ pub async fn register(
|
||||
let mut tx = pool
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let user = sqlx::query_as::<_, User>("SELECT * FROM fn_register_user($1, $2, $3, $4, $5)")
|
||||
.bind(&payload.email)
|
||||
@@ -2837,7 +2836,7 @@ pub async fn register(
|
||||
|
||||
tx.commit()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let token = create_jwt(user.id, user.organization_id, &user.role).map_err(|_| {
|
||||
(
|
||||
@@ -3049,7 +3048,7 @@ pub async fn get_course_analytics(
|
||||
let analytics = res
|
||||
.json::<CourseAnalytics>()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(analytics))
|
||||
}
|
||||
@@ -3097,7 +3096,7 @@ pub async fn get_advanced_analytics(
|
||||
let analytics = res
|
||||
.json::<common::models::AdvancedAnalytics>()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(analytics))
|
||||
}
|
||||
@@ -3128,7 +3127,7 @@ pub async fn get_lesson_heatmap(
|
||||
let heatmap = res
|
||||
.json::<Vec<common::models::HeatmapPoint>>()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(heatmap))
|
||||
}
|
||||
@@ -3172,7 +3171,7 @@ pub async fn get_audit_logs(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(logs))
|
||||
}
|
||||
@@ -3247,7 +3246,7 @@ pub async fn get_sso_config(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(config))
|
||||
}
|
||||
@@ -3306,7 +3305,7 @@ pub async fn update_sso_config(
|
||||
|
||||
// We use fetch_all + next for slightly better error handling in this complex query
|
||||
let config = config
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or((
|
||||
@@ -3327,7 +3326,7 @@ pub async fn sso_login_init(
|
||||
.bind(org_id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.ok_or((
|
||||
StatusCode::NOT_FOUND,
|
||||
"SSO no configurado o deshabilitado para esta organización".to_string(),
|
||||
@@ -3340,7 +3339,8 @@ pub async fn sso_login_init(
|
||||
)
|
||||
})?;
|
||||
|
||||
let provider_metadata = CoreProviderMetadata::discover_async(issuer_url, async_http_client)
|
||||
let http_client = reqwest::Client::new();
|
||||
let provider_metadata = CoreProviderMetadata::discover_async(issuer_url, &http_client)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
@@ -3359,7 +3359,7 @@ pub async fn sso_login_init(
|
||||
"{}/auth/sso/callback",
|
||||
env::var("CMS_API_URL").unwrap_or_else(|_| "http://localhost:3001".to_string())
|
||||
))
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?,
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?,
|
||||
);
|
||||
|
||||
let (auth_url, csrf_token, nonce) = client
|
||||
@@ -3380,7 +3380,7 @@ pub async fn sso_login_init(
|
||||
.bind(nonce.secret())
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(axum::response::Redirect::to(auth_url.as_str()))
|
||||
}
|
||||
@@ -3413,15 +3413,16 @@ pub async fn sso_callback(
|
||||
.bind(org_id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// 3. Exchange code for token
|
||||
let issuer_url = IssuerUrl::new(config.issuer_url.clone())
|
||||
.map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
|
||||
|
||||
let provider_metadata = CoreProviderMetadata::discover_async(issuer_url, async_http_client)
|
||||
let http_client = reqwest::Client::new();
|
||||
let provider_metadata = CoreProviderMetadata::discover_async(issuer_url, &http_client)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let client = CoreClient::from_provider_metadata(
|
||||
provider_metadata,
|
||||
@@ -3433,12 +3434,13 @@ pub async fn sso_callback(
|
||||
"{}/auth/sso/callback",
|
||||
env::var("CMS_API_URL").unwrap_or_else(|_| "http://localhost:3001".to_string())
|
||||
))
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?,
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?,
|
||||
);
|
||||
|
||||
let token_response = client
|
||||
.exchange_code(AuthorizationCode::new(params.code))
|
||||
.request_async(async_http_client)
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.request_async(&http_client)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
@@ -3464,7 +3466,7 @@ pub async fn sso_callback(
|
||||
.to_string();
|
||||
let name = claims
|
||||
.name()
|
||||
.and_then(|n| n.get(None))
|
||||
.and_then(|n| n.get(None::<&openidconnect::LanguageTag>))
|
||||
.map(|n| n.to_string())
|
||||
.unwrap_or_else(|| email.split('@').next().unwrap_or("User").to_string());
|
||||
|
||||
@@ -3472,7 +3474,7 @@ pub async fn sso_callback(
|
||||
let mut tx = pool
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let user = sqlx::query_as::<_, User>(
|
||||
"SELECT * FROM users WHERE organization_id = $1 AND lower(email) = lower($2)",
|
||||
@@ -3481,7 +3483,7 @@ pub async fn sso_callback(
|
||||
.bind(&email)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let user = match user {
|
||||
Some(u) => u,
|
||||
@@ -3499,13 +3501,13 @@ pub async fn sso_callback(
|
||||
.bind("student") // Default role
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
}
|
||||
};
|
||||
|
||||
tx.commit()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// 6. Generate JWT
|
||||
let token =
|
||||
@@ -3857,7 +3859,7 @@ pub async fn update_user(
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
}
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
log_action(
|
||||
&pool,
|
||||
@@ -3913,7 +3915,7 @@ pub async fn delete_user(
|
||||
.execute(&pool)
|
||||
.await
|
||||
}
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err((StatusCode::NOT_FOUND, "User not found".into()));
|
||||
@@ -3959,7 +3961,7 @@ pub async fn get_webhooks(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(webhooks))
|
||||
}
|
||||
@@ -3987,7 +3989,7 @@ pub async fn create_webhook(
|
||||
.bind(payload.secret)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
log_action(
|
||||
&pool,
|
||||
@@ -4018,7 +4020,7 @@ pub async fn delete_webhook(
|
||||
.bind(org_ctx.id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err((StatusCode::NOT_FOUND, "Webhook not found".into()));
|
||||
@@ -4575,7 +4577,7 @@ pub async fn check_course_access(
|
||||
.bind(user_id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(exists)
|
||||
}
|
||||
@@ -4612,7 +4614,7 @@ pub async fn get_course_team(
|
||||
.bind(course_id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(team))
|
||||
}
|
||||
@@ -4641,7 +4643,7 @@ pub async fn add_team_member(
|
||||
.bind(claims.sub)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
};
|
||||
|
||||
if !is_authorized {
|
||||
@@ -4686,7 +4688,7 @@ pub async fn remove_team_member(
|
||||
.bind(claims.sub)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
};
|
||||
|
||||
if !is_authorized && claims.sub != user_id {
|
||||
@@ -4698,7 +4700,7 @@ pub async fn remove_team_member(
|
||||
.bind(user_id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
@@ -4715,7 +4717,7 @@ pub async fn create_course_preview_token(
|
||||
}
|
||||
|
||||
let token = create_preview_token(claims.sub, claims.org, id)
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(json!({ "token": token })))
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ pub async fn retry_task(
|
||||
.bind(id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if let Some(l) = lesson {
|
||||
let pool_clone = pool.clone();
|
||||
@@ -134,7 +134,7 @@ pub async fn retry_task(
|
||||
.bind(id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if let Some(task) = zip_task {
|
||||
let zip_batch_id_from_metadata = task
|
||||
@@ -176,7 +176,7 @@ pub async fn retry_task(
|
||||
.bind(task.created_at)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if candidates.len() == 1 {
|
||||
candidates[0].zip_batch_id
|
||||
@@ -200,7 +200,7 @@ pub async fn retry_task(
|
||||
.bind(zip_batch_id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if assets.is_empty() {
|
||||
return Err((
|
||||
@@ -306,7 +306,7 @@ pub async fn cancel_task(
|
||||
.bind(id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if lesson_result.rows_affected() > 0 {
|
||||
return Ok(StatusCode::NO_CONTENT);
|
||||
@@ -327,7 +327,7 @@ pub async fn cancel_task(
|
||||
.bind(id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if task_result.rows_affected() > 0 {
|
||||
return Ok(StatusCode::NO_CONTENT);
|
||||
|
||||
@@ -40,7 +40,7 @@ pub async fn get_token_usage(
|
||||
)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener el uso: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Calcular estadísticas
|
||||
let total_tokens: i64 = usage.iter().map(|u| u.total_tokens).sum();
|
||||
@@ -123,7 +123,7 @@ pub async fn get_ai_usage_dashboard(
|
||||
.bind(filters.end_date.clone())
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener el uso diario: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Obtener uso por punto de conexión/función
|
||||
let by_endpoint: Vec<UsageByEndpoint> = sqlx::query_as(
|
||||
@@ -149,7 +149,7 @@ pub async fn get_ai_usage_dashboard(
|
||||
.bind(filters.end_date.clone())
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener el uso del punto de conexión: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Obtener usuarios principales
|
||||
let top_users: Vec<TopUserUsage> = sqlx::query_as(
|
||||
@@ -179,7 +179,7 @@ pub async fn get_ai_usage_dashboard(
|
||||
.bind(filters.end_date.clone())
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los usuarios principales: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Calcular estadísticas de resumen
|
||||
let total_tokens: i64 = daily_usage.iter().map(|d| d.total_tokens).sum();
|
||||
@@ -256,7 +256,7 @@ pub async fn get_ai_usage_logs(
|
||||
.bind(offset as i64)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los logs: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Obtener el conteo total para la paginación
|
||||
let count: (i64,) = sqlx::query_as(
|
||||
@@ -274,7 +274,7 @@ pub async fn get_ai_usage_logs(
|
||||
.bind(filters.user_id.clone())
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al contar los logs: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(UsageLogsResponse {
|
||||
logs,
|
||||
@@ -459,7 +459,7 @@ pub async fn get_ai_usage_global(
|
||||
.bind(filters.end_date.clone())
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener el uso diario: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Obtener uso por punto de conexión/función
|
||||
let by_endpoint: Vec<UsageByEndpoint> = sqlx::query_as(
|
||||
@@ -483,7 +483,7 @@ pub async fn get_ai_usage_global(
|
||||
.bind(filters.end_date.clone())
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener el uso del punto de conexión: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Obtener uso por organización
|
||||
let by_organization: Vec<UsageByOrganization> = sqlx::query_as(
|
||||
@@ -509,7 +509,7 @@ pub async fn get_ai_usage_global(
|
||||
.bind(filters.end_date.clone())
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener el uso de la organización: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Obtener usuarios principales en todas las organizaciones
|
||||
let top_users: Vec<TopUserUsage> = sqlx::query_as(
|
||||
@@ -539,7 +539,7 @@ pub async fn get_ai_usage_global(
|
||||
.bind(filters.end_date.clone())
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los usuarios principales: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Obtener uso por tipo de solicitud (para gráfico circular)
|
||||
let by_request_type: Vec<UsageByRequestType> = sqlx::query_as(
|
||||
@@ -560,7 +560,7 @@ pub async fn get_ai_usage_global(
|
||||
.bind(filters.end_date.clone())
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener el uso del tipo de solicitud: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Calcular estadísticas de resumen
|
||||
let total_tokens: i64 = daily_usage.iter().map(|d| d.total_tokens).sum();
|
||||
@@ -593,7 +593,7 @@ pub async fn get_ai_usage_global(
|
||||
.bind(filters.end_date.clone())
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al contar los usuarios: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Calcular uso específico por estudiante (interacciones de chat)
|
||||
let student_chat_usage: Vec<StudentChatUsage> = sqlx::query_as(
|
||||
@@ -623,7 +623,7 @@ pub async fn get_ai_usage_global(
|
||||
.bind(filters.end_date.clone())
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener el uso de chat de estudiantes: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Calcular totales de chat de estudiantes
|
||||
let total_student_chat_tokens: i64 = student_chat_usage.iter().map(|s| s.total_tokens).sum();
|
||||
@@ -757,7 +757,7 @@ pub async fn set_user_token_limit(
|
||||
.bind(user_id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al establecer el límite: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
@@ -792,7 +792,7 @@ pub async fn get_user_token_usage(
|
||||
.bind(user_id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener el uso: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(usage))
|
||||
}
|
||||
@@ -810,7 +810,7 @@ pub async fn check_user_token_limit(
|
||||
.bind(user_id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to check limit: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ async fn maybe_push_local_file_to_s3(
|
||||
|
||||
let bytes = tokio::fs::read(local_path)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al leer el archivo local: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let client = build_s3_client(&settings).await?;
|
||||
let key = build_s3_object_key(org_id, course_id, storage_filename);
|
||||
@@ -339,13 +339,13 @@ pub async fn public_s3_proxy(
|
||||
header::CONTENT_TYPE,
|
||||
"application/octet-stream"
|
||||
.parse()
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Cabecera inválida: {}", e)))?,
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?,
|
||||
);
|
||||
headers.insert(
|
||||
header::CACHE_CONTROL,
|
||||
"public, max-age=3600"
|
||||
.parse()
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Cabecera inválida: {}", e)))?,
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?,
|
||||
);
|
||||
|
||||
Ok((headers, bytes))
|
||||
@@ -375,7 +375,7 @@ async fn read_storage_bytes(storage_path: &str) -> Result<Vec<u8>, (StatusCode,
|
||||
|
||||
tokio::fs::read(storage_path)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error de lectura: {}", e)))
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))
|
||||
}
|
||||
|
||||
/// POST /api/assets/upload - Subir un archivo a la biblioteca global
|
||||
@@ -408,7 +408,7 @@ pub async fn upload_asset(
|
||||
data = field
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.to_vec();
|
||||
} else if name == "course_id" {
|
||||
if let Ok(txt) = field.text().await {
|
||||
@@ -447,7 +447,7 @@ pub async fn upload_asset(
|
||||
// Asegurar que el directorio de subidas existe
|
||||
tokio::fs::create_dir_all("uploads")
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let (storage_filename, storage_path, stored_filename, stored_mimetype) =
|
||||
if is_flv_media(&filename, &mimetype) {
|
||||
@@ -455,7 +455,7 @@ pub async fn upload_asset(
|
||||
let temp_storage_path = format!("uploads/{}", temp_storage_filename);
|
||||
tokio::fs::write(&temp_storage_path, data)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let final_storage_filename = format!("{}.mp4", asset_id);
|
||||
let final_storage_path = format!("uploads/{}", final_storage_filename);
|
||||
@@ -483,7 +483,7 @@ pub async fn upload_asset(
|
||||
|
||||
tokio::fs::write(&storage_path, data)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
(storage_filename, storage_path, filename.clone(), mimetype.clone())
|
||||
};
|
||||
@@ -528,7 +528,7 @@ pub async fn upload_asset(
|
||||
.bind(size_bytes)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(AssetUploadResponse {
|
||||
id: asset_id,
|
||||
@@ -614,7 +614,7 @@ pub async fn list_assets(
|
||||
.bind(offset)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(assets))
|
||||
}
|
||||
@@ -645,7 +645,7 @@ pub async fn list_asset_import_history(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(items))
|
||||
}
|
||||
@@ -664,7 +664,7 @@ pub async fn delete_asset(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "Activo no encontrado".to_string()))?;
|
||||
|
||||
// 2. Eliminar de la base de datos
|
||||
@@ -672,7 +672,7 @@ pub async fn delete_asset(
|
||||
.bind(id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// 3. Eliminar archivo físico u objeto de S3
|
||||
let _ = delete_storage_path(&asset.storage_path).await;
|
||||
@@ -790,7 +790,7 @@ pub async fn ingest_asset_for_rag(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let source_kind = if asset.mimetype.starts_with("audio/") || asset.mimetype.starts_with("video/") {
|
||||
"audio-transcription"
|
||||
@@ -1214,17 +1214,17 @@ pub async fn import_assets_zip(
|
||||
let temp_name = format!("uploads/tmp/import-{}.zip", Uuid::new_v4());
|
||||
tokio::fs::create_dir_all("uploads/tmp")
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to create temp dir: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let mut temp_file = tokio::fs::File::create(&temp_name)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to create temp zip file: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
let mut received_bytes: u64 = 0;
|
||||
|
||||
while let Some(chunk) = field
|
||||
.chunk()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to read upload chunk: {}", e)))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
{
|
||||
received_bytes = received_bytes.saturating_add(chunk.len() as u64);
|
||||
if received_bytes > max_upload_bytes {
|
||||
@@ -1241,13 +1241,13 @@ pub async fn import_assets_zip(
|
||||
temp_file
|
||||
.write_all(&chunk)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to write temp zip file: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
}
|
||||
|
||||
temp_file
|
||||
.flush()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to flush temp zip file: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
zip_temp_path = Some(temp_name);
|
||||
} else if name == "course_id" {
|
||||
@@ -1315,7 +1315,7 @@ pub async fn import_assets_zip(
|
||||
let source_zip_name = zip_original_name.unwrap_or_else(|| "import.zip".to_string());
|
||||
|
||||
let zip_file = std::fs::File::open(&zip_path)
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to open temp zip file: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let mut archive = zip::ZipArchive::new(zip_file)
|
||||
.map_err(|_| (StatusCode::BAD_REQUEST, "Invalid ZIP file".to_string()))?;
|
||||
@@ -1334,7 +1334,7 @@ pub async fn import_assets_zip(
|
||||
for i in 0..len {
|
||||
let mut file = archive
|
||||
.by_index(i)
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("ZIP read error: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if !file.is_file() {
|
||||
continue;
|
||||
@@ -1377,7 +1377,7 @@ pub async fn import_assets_zip(
|
||||
|
||||
let mut content = Vec::new();
|
||||
std::io::Read::read_to_end(&mut file, &mut content)
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("ZIP entry read failed: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let guessed_mimetype = mime_guess::from_path(&safe_filename)
|
||||
.first_or_octet_stream()
|
||||
@@ -1435,7 +1435,7 @@ pub async fn import_assets_zip(
|
||||
|
||||
tokio::fs::create_dir_all("uploads")
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// ── Phase 2: process entries ───────────────────────────────────────────────
|
||||
let mut imported_assets = 0usize;
|
||||
@@ -1564,7 +1564,7 @@ pub async fn import_assets_zip(
|
||||
let temp_storage_path = format!("uploads/tmp/{}", temp_storage_filename);
|
||||
tokio::fs::create_dir_all("uploads/tmp")
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error creating temp dir: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
if let Err(e) = tokio::fs::write(&temp_storage_path, &content).await {
|
||||
failed_entries.push(format!("{}: local write failed ({})", entry_name, e));
|
||||
continue;
|
||||
@@ -1573,7 +1573,7 @@ pub async fn import_assets_zip(
|
||||
let storage_path = build_ready_for_rag_path(org_ctx.id, asset_id, &format!("{}.mp4", asset_id));
|
||||
tokio::fs::create_dir_all(StdPath::new(&storage_path).parent().unwrap_or(StdPath::new(".")))
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error creating ready-for-rag dir: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if let Err((_, msg)) = transcode_flv_to_mp4(&temp_storage_path, &storage_path).await {
|
||||
let _ = tokio::fs::remove_file(&temp_storage_path).await;
|
||||
@@ -1591,7 +1591,7 @@ pub async fn import_assets_zip(
|
||||
let temp_storage_path = format!("uploads/{}", temp_storage_filename);
|
||||
tokio::fs::write(&temp_storage_path, &content)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let final_storage_filename = format!("{}.mp4", asset_id);
|
||||
let final_storage_path = format!("uploads/{}", final_storage_filename);
|
||||
@@ -1609,7 +1609,7 @@ pub async fn import_assets_zip(
|
||||
let storage_path = build_ready_for_rag_path(org_ctx.id, asset_id, &safe_filename);
|
||||
tokio::fs::create_dir_all(StdPath::new(&storage_path).parent().unwrap_or(StdPath::new(".")))
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error creating ready-for-rag dir: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
if let Err(e) = tokio::fs::write(&storage_path, &content).await {
|
||||
failed_entries.push(format!("{}: local write failed ({})", entry_name, e));
|
||||
continue;
|
||||
@@ -2124,7 +2124,7 @@ async fn normalize_flv_asset_for_rag(
|
||||
|
||||
tokio::fs::create_dir_all("uploads/tmp")
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error creating temp dir: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let input_path = format!("uploads/tmp/flv-normalize-in-{}.flv", asset.id);
|
||||
let output_path = format!("uploads/tmp/flv-normalize-out-{}.mp4", asset.id);
|
||||
@@ -2132,7 +2132,7 @@ async fn normalize_flv_asset_for_rag(
|
||||
let source_bytes = read_storage_bytes(&asset.storage_path).await?;
|
||||
tokio::fs::write(&input_path, source_bytes)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error writing temp FLV: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if let Err(e) = transcode_flv_to_mp4(&input_path, &output_path).await {
|
||||
let _ = tokio::fs::remove_file(&input_path).await;
|
||||
@@ -2144,7 +2144,7 @@ async fn normalize_flv_asset_for_rag(
|
||||
|
||||
let output_bytes = tokio::fs::read(&output_path)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error reading temp MP4: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
let _ = tokio::fs::remove_file(&output_path).await;
|
||||
|
||||
let next_storage_path = replace_last_path_extension(&asset.storage_path, "mp4");
|
||||
@@ -2172,7 +2172,7 @@ async fn normalize_flv_asset_for_rag(
|
||||
} else {
|
||||
tokio::fs::write(&next_storage_path, &output_bytes)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error writing normalized MP4: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if asset.storage_path != next_storage_path {
|
||||
let _ = tokio::fs::remove_file(&asset.storage_path).await;
|
||||
@@ -2199,7 +2199,7 @@ async fn normalize_flv_asset_for_rag(
|
||||
.bind(asset.id)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error updating normalized asset: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
asset.filename = next_filename;
|
||||
asset.storage_path = next_storage_path;
|
||||
@@ -2307,7 +2307,7 @@ async fn ingest_chunks_to_question_bank(
|
||||
.bind(unit_number)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Insert failed: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if let Ok(embedding_res) = generate_embedding(client, ollama_url, model, chunk).await {
|
||||
let pgvector = ai::embedding_to_pgvector(&embedding_res.embedding);
|
||||
@@ -2371,10 +2371,10 @@ async fn extract_pdf_text_from_bytes(bytes: Vec<u8>) -> Result<String, (StatusCo
|
||||
let temp_name = format!("uploads/tmp-pdf-{}.pdf", Uuid::new_v4());
|
||||
tokio::fs::create_dir_all("uploads")
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Create temp dir failed: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
tokio::fs::write(&temp_name, bytes)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Write temp pdf failed: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let output = Command::new("pdftotext")
|
||||
.arg("-layout")
|
||||
@@ -2434,7 +2434,7 @@ async fn transcribe_media_bytes_with_override(
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(300))
|
||||
.build()
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Whisper HTTP client error: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let mut last_error = String::new();
|
||||
|
||||
@@ -2466,7 +2466,7 @@ async fn transcribe_media_bytes_with_override(
|
||||
let transcription: serde_json::Value = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Invalid Whisper response: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let text = transcription
|
||||
.get("text")
|
||||
|
||||
@@ -56,7 +56,7 @@ pub async fn generate_question_embeddings(
|
||||
.danger_accept_invalid_certs(true)
|
||||
.danger_accept_invalid_hostnames(true)
|
||||
.build()
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error del cliente HTTP: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let ollama_url = ai::get_ollama_url();
|
||||
let model = ai::get_embedding_model();
|
||||
@@ -74,7 +74,7 @@ pub async fn generate_question_embeddings(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let _total = questions.len();
|
||||
let mut processed = 0;
|
||||
@@ -168,7 +168,7 @@ pub async fn regenerate_question_embedding(
|
||||
.danger_accept_invalid_certs(true)
|
||||
.danger_accept_invalid_hostnames(true)
|
||||
.build()
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error del cliente HTTP: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let ollama_url = ai::get_ollama_url();
|
||||
let model = ai::get_embedding_model();
|
||||
@@ -181,7 +181,7 @@ pub async fn regenerate_question_embedding(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "Pregunta no encontrada".to_string()))?;
|
||||
|
||||
// Generar texto de la incrustación
|
||||
@@ -209,7 +209,7 @@ pub async fn regenerate_question_embedding(
|
||||
// Generar incrustación
|
||||
let response = generate_embedding(&client, &ollama_url, &model, &embedding_text)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error de IA: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let pgvector = ai::embedding_to_pgvector(&response.embedding);
|
||||
|
||||
@@ -226,7 +226,7 @@ pub async fn regenerate_question_embedding(
|
||||
.bind(question_id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
@@ -244,7 +244,7 @@ pub async fn semantic_search(
|
||||
.danger_accept_invalid_certs(true)
|
||||
.danger_accept_invalid_hostnames(true)
|
||||
.build()
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error del cliente HTTP: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let ollama_url = ai::get_ollama_url();
|
||||
let model = ai::get_embedding_model();
|
||||
@@ -252,7 +252,7 @@ pub async fn semantic_search(
|
||||
// Generar incrustación para la consulta
|
||||
let embedding_response = generate_embedding(&client, &ollama_url, &model, &filters.query)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error de IA: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let pgvector = ai::embedding_to_pgvector(&embedding_response.embedding);
|
||||
|
||||
@@ -310,7 +310,7 @@ pub async fn semantic_search(
|
||||
let results = sql_query
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(results))
|
||||
}
|
||||
@@ -348,7 +348,7 @@ pub async fn find_similar_questions(
|
||||
.bind(limit)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.into_iter()
|
||||
.filter(|r| r.similarity >= threshold)
|
||||
.collect();
|
||||
|
||||
@@ -40,7 +40,7 @@ pub async fn create_library_block(
|
||||
.bind(payload.tags.as_deref())
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(block))
|
||||
}
|
||||
@@ -98,7 +98,7 @@ pub async fn list_library_blocks(
|
||||
let blocks = sql_query
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(blocks))
|
||||
}
|
||||
@@ -116,7 +116,7 @@ pub async fn get_library_block(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
match block {
|
||||
Some(b) => Ok(Json(b)),
|
||||
@@ -137,7 +137,7 @@ pub async fn update_library_block(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if existing.is_none() {
|
||||
return Err((StatusCode::NOT_FOUND, "Bloque no encontrado".to_string()));
|
||||
@@ -163,7 +163,7 @@ pub async fn update_library_block(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
} else {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
@@ -181,7 +181,7 @@ pub async fn update_library_block(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
};
|
||||
|
||||
Ok(Json(updated))
|
||||
@@ -198,7 +198,7 @@ pub async fn delete_library_block(
|
||||
.bind(org_ctx.id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err((StatusCode::NOT_FOUND, "Bloque no encontrado".to_string()));
|
||||
@@ -218,7 +218,7 @@ pub async fn increment_block_usage(
|
||||
.bind(org_ctx.id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err((StatusCode::NOT_FOUND, "Bloque no encontrado".to_string()));
|
||||
|
||||
@@ -63,7 +63,7 @@ pub async fn list_plugins(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let plugins = rows
|
||||
.into_iter()
|
||||
@@ -101,7 +101,7 @@ pub async fn list_enabled_plugins(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let plugins = rows
|
||||
.into_iter()
|
||||
@@ -155,7 +155,7 @@ pub async fn create_plugin(
|
||||
.bind(payload.config.unwrap_or(serde_json::json!({})))
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok((StatusCode::CREATED, Json(OrgPlugin {
|
||||
id: row.get("id"),
|
||||
@@ -190,7 +190,7 @@ pub async fn update_plugin(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if !exists {
|
||||
return Err((StatusCode::NOT_FOUND, "Plugin no encontrado".to_string()));
|
||||
@@ -229,7 +229,7 @@ pub async fn update_plugin(
|
||||
.bind(payload.enabled)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(OrgPlugin {
|
||||
id: row.get("id"),
|
||||
@@ -262,7 +262,7 @@ pub async fn delete_plugin(
|
||||
.bind(org_ctx.id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err((StatusCode::NOT_FOUND, "Plugin no encontrado".to_string()));
|
||||
|
||||
@@ -92,7 +92,7 @@ async fn connect_mysql_pool(env_var: &str) -> Result<sqlx::MySqlPool, (StatusCod
|
||||
use sqlx::mysql::MySqlPoolOptions;
|
||||
|
||||
let mysql_url = std::env::var(env_var)
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, format!("{} no configurada", env_var)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let mut last_error = String::new();
|
||||
|
||||
@@ -379,7 +379,7 @@ pub async fn create_question(
|
||||
.bind(media_type.as_deref())
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(question))
|
||||
}
|
||||
@@ -501,7 +501,7 @@ pub async fn list_questions(
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
}
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(questions))
|
||||
}
|
||||
@@ -627,7 +627,7 @@ pub async fn delete_question(
|
||||
.bind(org_ctx.id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err((StatusCode::NOT_FOUND, "Pregunta no encontrada".to_string()));
|
||||
@@ -690,7 +690,7 @@ pub async fn import_from_mysql(
|
||||
)
|
||||
.fetch_all(&mysql_pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los cursos: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
tracing::info!("Se obtuvieron {} cursos de MySQL", mysql_courses.len());
|
||||
|
||||
@@ -698,7 +698,7 @@ pub async fn import_from_mysql(
|
||||
tracing::info!("Guardando planes y cursos en PostgreSQL...");
|
||||
save_mysql_courses_and_plans(&pool, org_ctx.id, mysql_plans, mysql_courses)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al guardar cursos/planes: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Obtener preguntas de MySQL
|
||||
let mysql_questions: Vec<MySqlQuestion> = if payload.import_all.unwrap_or(false) {
|
||||
@@ -718,7 +718,7 @@ pub async fn import_from_mysql(
|
||||
)
|
||||
.fetch_all(&mysql_pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener las preguntas: {}", e)))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
} else if let Some(course_id) = payload.mysql_course_id {
|
||||
// Importar todas las preguntas para un curso específico (sin límite)
|
||||
sqlx::query_as(
|
||||
@@ -737,7 +737,7 @@ pub async fn import_from_mysql(
|
||||
.bind(course_id)
|
||||
.fetch_all(&mysql_pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener las preguntas: {}", e)))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
} else if let Some(question_ids) = payload.question_ids {
|
||||
// Obtener IDs de preguntas específicas - usar enfoque simple
|
||||
let mut imported_questions: Vec<QuestionBank> = vec![];
|
||||
@@ -758,7 +758,7 @@ pub async fn import_from_mysql(
|
||||
.bind(q_id)
|
||||
.fetch_optional(&mysql_pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener la pregunta {}: {}", q_id, e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if let Some(question) = mq {
|
||||
// Mapear el tipo de pregunta de MySQL al tipo de pregunta de la plataforma
|
||||
@@ -806,7 +806,7 @@ pub async fn import_from_mysql(
|
||||
.bind(&source_metadata)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al importar la pregunta {}: {}", question.id_pregunta, e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
imported_questions.push(qb);
|
||||
}
|
||||
@@ -837,7 +837,7 @@ pub async fn import_from_mysql(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al comprobar existencia: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if exists.0 {
|
||||
skipped_count += 1;
|
||||
@@ -893,7 +893,7 @@ pub async fn import_from_mysql(
|
||||
.bind(mq.id_cursos)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al importar la pregunta {}: {}", mq.id_pregunta, e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
imported_questions.push(question);
|
||||
}
|
||||
@@ -1041,7 +1041,7 @@ pub async fn list_mysql_courses(
|
||||
)
|
||||
.fetch_all(&mysql_pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los cursos: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
mysql_pool.close().await;
|
||||
|
||||
@@ -1067,7 +1067,7 @@ pub async fn get_mysql_plans(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los planes: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Respaldo compatible con versiones anteriores: si el reflejo SAM está vacío, usar el reflejo de metadatos heredado.
|
||||
if plans.is_empty() {
|
||||
@@ -1084,7 +1084,7 @@ pub async fn get_mysql_plans(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los planes heredados: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
}
|
||||
|
||||
// Auto-sincronización de último recurso: si sigue vacío, obtener metadatos de MySQL y persistirlos.
|
||||
@@ -1208,7 +1208,7 @@ pub async fn get_mysql_courses_by_plan(
|
||||
.bind(filters.plan_id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los cursos: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Intentar refrescar desde MySQL (fuente SAM) para evitar listas incompletas por espejo desactualizado.
|
||||
if let Ok(mysql_pool) = connect_mysql_pool("MYSQL_DATABASE_URL").await {
|
||||
@@ -1315,7 +1315,7 @@ pub async fn get_mysql_courses_by_plan(
|
||||
.bind(filters.plan_id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los cursos heredados: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
}
|
||||
|
||||
Ok(Json(courses))
|
||||
@@ -1408,7 +1408,7 @@ pub async fn import_all_from_mysql(
|
||||
)
|
||||
.fetch_all(&mysql_pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los cursos: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
tracing::info!("Se obtuvieron {} cursos de MySQL", mysql_courses.len());
|
||||
|
||||
@@ -1416,7 +1416,7 @@ pub async fn import_all_from_mysql(
|
||||
tracing::info!("Guardando planes y cursos en PostgreSQL...");
|
||||
save_mysql_courses_and_plans(&pool, org_ctx.id, mysql_plans.clone(), mysql_courses.clone())
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al guardar cursos/planes: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Si solo se solicita la importación de metadatos, salir antes
|
||||
if import_metadata_only {
|
||||
@@ -1477,7 +1477,7 @@ pub async fn import_all_from_mysql(
|
||||
)
|
||||
.fetch_all(&mysql_pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener las preguntas: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
mysql_pool.close().await;
|
||||
|
||||
@@ -1505,7 +1505,7 @@ pub async fn import_all_from_mysql(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Fallo en la comprobación: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
match existing {
|
||||
Some((id, is_active)) => {
|
||||
@@ -1724,14 +1724,14 @@ pub async fn ai_generate_question(
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("La solicitud de IA falló: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Error de la API de IA: {}", response.status())));
|
||||
return Err((StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()));
|
||||
}
|
||||
|
||||
let result: serde_json::Value = response.json().await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al analizar la respuesta de la IA: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Extraer contenido de la respuesta de Ollama
|
||||
let content = result
|
||||
@@ -1742,7 +1742,7 @@ pub async fn ai_generate_question(
|
||||
|
||||
// Analizar la respuesta de la IA como JSON
|
||||
let ai_question: AIQuestionResponse = serde_json::from_str(content)
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al analizar el JSON de la pregunta: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(ai_question))
|
||||
}
|
||||
@@ -1801,7 +1801,7 @@ pub async fn import_course_from_mysql(
|
||||
if let sqlx::Error::RowNotFound = e {
|
||||
(StatusCode::NOT_FOUND, format!("Curso con ID {} no encontrado en MySQL", payload.mysql_course_id))
|
||||
} else {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener el curso de MySQL: {}", e))
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string())
|
||||
}
|
||||
})?;
|
||||
|
||||
@@ -1809,7 +1809,7 @@ pub async fn import_course_from_mysql(
|
||||
|
||||
// Iniciar transacción
|
||||
let mut tx = pool.begin().await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al iniciar la transacción: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Determinar el tipo y nivel del curso para la generación de la estructura
|
||||
let course_type = calculate_course_type_from_duration(mysql_course.duracion);
|
||||
@@ -1845,7 +1845,7 @@ pub async fn import_course_from_mysql(
|
||||
.bind(mysql_course.id_cursos)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al crear el curso: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
tracing::info!("Curso creado en PostgreSQL: {}", new_course.id);
|
||||
|
||||
@@ -1863,7 +1863,7 @@ pub async fn import_course_from_mysql(
|
||||
|
||||
// Confirmar transacción
|
||||
tx.commit().await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al confirmar la transacción: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
tracing::info!(
|
||||
"Curso {} importado con éxito con {} módulos y {} lecciones",
|
||||
|
||||
@@ -121,7 +121,7 @@ pub async fn create_rubric(
|
||||
.bind(&payload.description)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(rubric))
|
||||
}
|
||||
@@ -144,7 +144,7 @@ pub async fn list_course_rubrics(
|
||||
.bind(course_id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(rubrics))
|
||||
}
|
||||
@@ -167,7 +167,7 @@ pub async fn get_rubric_with_details(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "Rúbrica no encontrada".to_string()))?;
|
||||
|
||||
// Get criteria
|
||||
@@ -182,7 +182,7 @@ pub async fn get_rubric_with_details(
|
||||
.bind(rubric_id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Get levels for each criterion
|
||||
let mut criteria_with_levels = Vec::new();
|
||||
@@ -198,7 +198,7 @@ pub async fn get_rubric_with_details(
|
||||
.bind(criterion.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
criteria_with_levels.push(CriterionWithLevels { criterion, levels });
|
||||
}
|
||||
@@ -232,7 +232,7 @@ pub async fn update_rubric(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "Rúbrica no encontrada".to_string()))?;
|
||||
|
||||
Ok(Json(rubric))
|
||||
@@ -249,7 +249,7 @@ pub async fn delete_rubric(
|
||||
.bind(org_ctx.id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err((StatusCode::NOT_FOUND, "Rúbrica no encontrada".to_string()));
|
||||
@@ -273,7 +273,7 @@ pub async fn create_criterion(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "Rúbrica no encontrada".to_string()))?;
|
||||
|
||||
let position = payload.position.unwrap_or(0);
|
||||
@@ -292,7 +292,7 @@ pub async fn create_criterion(
|
||||
.bind(position)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Update rubric total_points
|
||||
let _= sqlx::query(
|
||||
@@ -306,7 +306,7 @@ pub async fn create_criterion(
|
||||
.bind(rubric_id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(criterion))
|
||||
}
|
||||
@@ -338,7 +338,7 @@ pub async fn update_criterion(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "Criterio no encontrado".to_string()))?;
|
||||
|
||||
// Update rubric total_points if max_points changed
|
||||
@@ -354,7 +354,7 @@ pub async fn update_criterion(
|
||||
.bind(criterion.rubric_id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
}
|
||||
|
||||
Ok(Json(criterion))
|
||||
@@ -371,7 +371,7 @@ pub async fn delete_criterion(
|
||||
.bind(criterion_id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "Criterio no encontrado".to_string()))?;
|
||||
|
||||
let rubric_id: Uuid = criterion_row.get("rubric_id");
|
||||
@@ -387,7 +387,7 @@ pub async fn delete_criterion(
|
||||
.bind(org_ctx.id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err((StatusCode::NOT_FOUND, "Criterio no encontrado".to_string()));
|
||||
@@ -405,7 +405,7 @@ pub async fn delete_criterion(
|
||||
.bind(rubric_id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
@@ -425,7 +425,7 @@ pub async fn create_level(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "Criterio no encontrado".to_string()))?;
|
||||
|
||||
let position = payload.position.unwrap_or(0);
|
||||
@@ -444,7 +444,7 @@ pub async fn create_level(
|
||||
.bind(position)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(level))
|
||||
}
|
||||
@@ -479,7 +479,7 @@ pub async fn update_level(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "Nivel no encontrado".to_string()))?;
|
||||
|
||||
Ok(Json(level))
|
||||
@@ -505,7 +505,7 @@ pub async fn delete_level(
|
||||
.bind(org_ctx.id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err((StatusCode::NOT_FOUND, "Nivel no encontrado".to_string()));
|
||||
@@ -534,7 +534,7 @@ pub async fn assign_rubric_to_lesson(
|
||||
.bind(rubric_id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(lesson_rubric))
|
||||
}
|
||||
@@ -550,7 +550,7 @@ pub async fn unassign_rubric_from_lesson(
|
||||
.bind(rubric_id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err((StatusCode::NOT_FOUND, "Vínculo lección-rúbrica no encontrado".to_string()));
|
||||
@@ -578,7 +578,7 @@ pub async fn get_lesson_rubrics(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(rubrics))
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ pub async fn sync_sam_students(
|
||||
|
||||
let sam_pool = sqlx::PgPool::connect(&sam_url)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al conectar con la base de datos SAM: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let mut errors = Vec::new();
|
||||
let mut students_synced = 0;
|
||||
@@ -85,7 +85,7 @@ pub async fn sync_sam_students(
|
||||
)
|
||||
.fetch_all(&sam_pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener estudiantes de SAM: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Convertir a SamStudentInfo
|
||||
let sam_students: Vec<SamStudentInfo> = rows.iter().map(|row| {
|
||||
@@ -196,7 +196,7 @@ pub async fn sync_sam_assignments(
|
||||
|
||||
let sam_pool = sqlx::PgPool::connect(&sam_url)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al conectar con la base de datos SAM: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let mut errors = Vec::new();
|
||||
let mut assignments_synced = 0;
|
||||
@@ -215,7 +215,7 @@ pub async fn sync_sam_assignments(
|
||||
)
|
||||
.fetch_all(&sam_pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener asignaciones: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Convertir a SamAssignmentInfo
|
||||
let sam_assignments: Vec<SamAssignmentInfo> = rows.iter().map(|row| {
|
||||
@@ -317,7 +317,7 @@ pub async fn list_sam_students(
|
||||
let rows = sqlx::query(&query)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los estudiantes: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let students: Vec<serde_json::Value> = rows.iter().map(|row| {
|
||||
serde_json::json!({
|
||||
@@ -354,7 +354,7 @@ pub async fn get_sam_student_courses(
|
||||
.bind(&sam_student_id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los cursos: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let courses: Vec<serde_json::Value> = rows.iter().map(|row| {
|
||||
serde_json::json!({
|
||||
@@ -386,7 +386,7 @@ pub async fn sync_all_sam(
|
||||
|
||||
let sam_pool = sqlx::PgPool::connect(&sam_url)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al conectar con la base de datos SAM: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Sincronizar estudiantes primero
|
||||
{
|
||||
@@ -400,7 +400,7 @@ pub async fn sync_all_sam(
|
||||
)
|
||||
.fetch_all(&sam_pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener estudiantes de SAM: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let sam_students: Vec<SamStudentInfo> = rows.iter().map(|row| {
|
||||
SamStudentInfo {
|
||||
@@ -470,7 +470,7 @@ pub async fn sync_all_sam(
|
||||
)
|
||||
.fetch_all(&sam_pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener asignaciones: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let sam_assignments: Vec<SamAssignmentInfo> = rows.iter().map(|row| {
|
||||
SamAssignmentInfo {
|
||||
|
||||
@@ -108,7 +108,7 @@ pub async fn create_test_template(
|
||||
.bind(payload.tags.as_deref())
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(template))
|
||||
}
|
||||
@@ -198,7 +198,7 @@ pub async fn list_test_templates(
|
||||
let templates = sql_query
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(templates))
|
||||
}
|
||||
@@ -240,7 +240,7 @@ pub async fn get_test_template(
|
||||
.bind(template_id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Obtener preguntas
|
||||
let questions: Vec<TestTemplateQuestion> = sqlx::query_as(
|
||||
@@ -255,7 +255,7 @@ pub async fn get_test_template(
|
||||
.bind(template_id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(TestTemplateWithQuestions {
|
||||
template,
|
||||
@@ -341,7 +341,7 @@ pub async fn delete_test_template(
|
||||
.bind(org_ctx.id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err((StatusCode::NOT_FOUND, "Plantilla no encontrada".to_string()));
|
||||
@@ -367,7 +367,7 @@ pub async fn create_template_question(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if !exists.0 {
|
||||
return Err((StatusCode::NOT_FOUND, "Plantilla no encontrada".to_string()));
|
||||
@@ -396,7 +396,7 @@ pub async fn create_template_question(
|
||||
.bind(payload.metadata.as_ref())
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(question))
|
||||
}
|
||||
@@ -428,7 +428,7 @@ pub async fn delete_template_question(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if !exists.0 {
|
||||
return Err((StatusCode::NOT_FOUND, "Plantilla no encontrada".to_string()));
|
||||
@@ -444,7 +444,7 @@ pub async fn delete_template_question(
|
||||
.bind(template_id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err((StatusCode::NOT_FOUND, "Pregunta no encontrada".to_string()));
|
||||
@@ -470,7 +470,7 @@ pub async fn create_template_section(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if !exists.0 {
|
||||
return Err((StatusCode::NOT_FOUND, "Plantilla no encontrada".to_string()));
|
||||
@@ -494,7 +494,7 @@ pub async fn create_template_section(
|
||||
.bind(payload.section_data.as_ref())
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(section))
|
||||
}
|
||||
@@ -525,7 +525,7 @@ pub async fn delete_template_section(
|
||||
.bind(template_id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err((StatusCode::NOT_FOUND, "Sección no encontrada".to_string()));
|
||||
@@ -571,7 +571,7 @@ pub async fn apply_template_to_lesson(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if !lesson_exists.0 {
|
||||
return Err((StatusCode::NOT_FOUND, "Lección no encontrada".to_string()));
|
||||
@@ -590,7 +590,7 @@ pub async fn apply_template_to_lesson(
|
||||
.bind(template_id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if template_questions.is_empty() {
|
||||
return Err((StatusCode::BAD_REQUEST, "La plantilla no tiene preguntas".to_string()));
|
||||
@@ -647,14 +647,14 @@ pub async fn apply_template_to_lesson(
|
||||
.bind(org_ctx.id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Incrementar el contador de uso de la plantilla
|
||||
sqlx::query("SELECT increment_template_usage($1)")
|
||||
.bind(template_id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
tracing::info!(
|
||||
"Plantilla '{}' aplicada a la lección '{}' con {} preguntas",
|
||||
@@ -1019,7 +1019,7 @@ pub async fn generate_questions_with_rag(
|
||||
.danger_accept_invalid_certs(true)
|
||||
.danger_accept_invalid_hostnames(true)
|
||||
.build()
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error del cliente HTTP: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let ollama_url = ai::get_ollama_url();
|
||||
let model = ai::get_embedding_model();
|
||||
@@ -1070,7 +1070,7 @@ pub async fn generate_questions_with_rag(
|
||||
.bind(requested_num_questions * 3) // Get more for diversity
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("La búsqueda semántica falló: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
tracing::info!("La búsqueda semántica encontró {} preguntas similares", mysql_questions.len());
|
||||
|
||||
@@ -1123,7 +1123,7 @@ pub async fn generate_questions_with_rag(
|
||||
.bind(requested_num_questions * 3)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("El recurso a palabras clave falló: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if mysql_questions.is_empty() {
|
||||
tracing::info!(
|
||||
@@ -1220,7 +1220,7 @@ pub async fn generate_questions_with_rag(
|
||||
.bind(requested_num_questions * 3)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("La búsqueda por palabras clave falló: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if mysql_questions.is_empty() {
|
||||
tracing::info!(
|
||||
@@ -1309,7 +1309,7 @@ pub async fn generate_questions_with_rag(
|
||||
.bind(course_id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener las preguntas: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
} else {
|
||||
// Fetch all imported MySQL questions for this organization
|
||||
// NO LIMIT - fetch all questions for better RAG context
|
||||
@@ -1339,7 +1339,7 @@ pub async fn generate_questions_with_rag(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener las preguntas: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
}
|
||||
|
||||
if mysql_questions.is_empty() {
|
||||
@@ -1487,7 +1487,7 @@ pub async fn generate_questions_with_rag(
|
||||
|
||||
let response = request.send().await.map_err(|e| {
|
||||
tracing::error!("AI request failed after timeout: {}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("Ollama timeout - el equipo t-800 está tardando en responder. Intenta nuevamente: {}", e))
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string())
|
||||
})?;
|
||||
|
||||
tracing::info!("Estado de la respuesta de Ollama: {}", response.status());
|
||||
|
||||
@@ -154,7 +154,7 @@ pub async fn get_me(
|
||||
.bind(claims.sub)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if let Some(user) = user {
|
||||
return Ok(Json(UserResponse {
|
||||
@@ -513,7 +513,7 @@ pub async fn export_course_grades(
|
||||
.bind(course_id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// 2. Obtener datos generales de los estudiantes
|
||||
#[derive(sqlx::FromRow)]
|
||||
@@ -546,7 +546,7 @@ pub async fn export_course_grades(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// 3. Obtener calificaciones detalladas por usuario/categoría
|
||||
#[derive(sqlx::FromRow)]
|
||||
@@ -571,7 +571,7 @@ pub async fn export_course_grades(
|
||||
.bind(course_id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// 4. Construir CSV
|
||||
let mut csv = "Name,Email,Cohort,Progress,Overall Score".to_string();
|
||||
@@ -830,7 +830,7 @@ pub async fn register(
|
||||
|
||||
// Validar formato de email (validación básica)
|
||||
let email_regex = regex::Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Email validation error".into()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
if !email_regex.is_match(&payload.email) {
|
||||
return Err((StatusCode::BAD_REQUEST, "Formato de email inválido".into()));
|
||||
}
|
||||
@@ -855,7 +855,7 @@ pub async fn register(
|
||||
let mut tx = pool
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let organization = if let Some(org_name) = payload.organization_name {
|
||||
sqlx::query_as::<_, Organization>(
|
||||
@@ -864,7 +864,7 @@ pub async fn register(
|
||||
.bind(&org_name)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al buscar o crear la organización: {}", e)))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
} else {
|
||||
sqlx::query_as::<_, Organization>(
|
||||
"SELECT * FROM organizations WHERE id = '00000000-0000-0000-0000-000000000001'",
|
||||
@@ -892,7 +892,7 @@ pub async fn register(
|
||||
|
||||
tx.commit()
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let token = create_jwt(user.id, user.organization_id, "student").map_err(|_| {
|
||||
(
|
||||
@@ -1872,7 +1872,7 @@ pub async fn submit_lesson_score(
|
||||
let mut tx = pool
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let ip = headers
|
||||
.get("x-forwarded-for")
|
||||
@@ -1894,7 +1894,7 @@ pub async fn submit_lesson_score(
|
||||
Some("EVENTO_DEL_SISTEMA".to_string()),
|
||||
)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// 1. Obtener reglas de intentos de la lección
|
||||
let max_attempts: Option<Option<i32>> =
|
||||
@@ -1902,7 +1902,7 @@ pub async fn submit_lesson_score(
|
||||
.bind(payload.lesson_id)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if max_attempts.is_none() {
|
||||
return Err((StatusCode::NOT_FOUND, "Lección no encontrada".into()));
|
||||
@@ -1916,7 +1916,7 @@ pub async fn submit_lesson_score(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if let Some(count) = existing_attempts {
|
||||
if let Some(max) = max_attempts {
|
||||
@@ -1941,7 +1941,7 @@ pub async fn submit_lesson_score(
|
||||
.bind(payload.metadata)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// 3.1 Sincronizar con MySQL externo si está disponible
|
||||
if let Some(mysql_pool) = mysql_pool {
|
||||
@@ -2006,7 +2006,7 @@ pub async fn submit_lesson_score(
|
||||
|
||||
tx.commit()
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// 4. Enviar Webhooks
|
||||
let webhook_service = common::webhooks::WebhookService::new(pool.clone());
|
||||
@@ -2217,7 +2217,7 @@ pub async fn get_course_analytics(
|
||||
.bind(filter.cohort_id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// 2. Puntaje promedio del curso (General)
|
||||
let average_score: Option<f32> = sqlx::query_scalar(
|
||||
@@ -2236,7 +2236,7 @@ pub async fn get_course_analytics(
|
||||
.bind(filter.cohort_id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// 3. Analítica por lección
|
||||
// Nota: Convertimos AVG a float4 para compatibilidad con PostgreSQL
|
||||
@@ -2262,7 +2262,7 @@ pub async fn get_course_analytics(
|
||||
.bind(filter.cohort_id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let lessons = rows
|
||||
.into_iter()
|
||||
@@ -2305,7 +2305,7 @@ pub async fn notify_student(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
match role.as_deref() {
|
||||
Some("instructor") | Some("admin") => {}
|
||||
@@ -2320,7 +2320,7 @@ pub async fn notify_student(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if !enrolled {
|
||||
return Err((StatusCode::NOT_FOUND, "El alumno no está inscrito en este curso".to_string()));
|
||||
@@ -2337,7 +2337,7 @@ pub async fn notify_student(
|
||||
.bind(&payload.link_url)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
@@ -2368,7 +2368,7 @@ pub async fn get_course_progress(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.flatten();
|
||||
|
||||
let progress_percentage = enrollment_progress
|
||||
@@ -2737,7 +2737,7 @@ pub async fn toggle_bookmark(
|
||||
.bind(lesson_id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if let Some(id) = existing_id {
|
||||
// Eliminar marcador
|
||||
@@ -2745,7 +2745,7 @@ pub async fn toggle_bookmark(
|
||||
.bind(id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
} else {
|
||||
// Añadir marcador
|
||||
@@ -2758,7 +2758,7 @@ pub async fn toggle_bookmark(
|
||||
.bind(lesson_id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
Ok(StatusCode::CREATED)
|
||||
}
|
||||
}
|
||||
@@ -2780,7 +2780,7 @@ pub async fn get_user_bookmarks(
|
||||
// Wait, let's create a better filter for this.
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(bookmarks))
|
||||
}
|
||||
@@ -2812,7 +2812,7 @@ pub async fn update_user(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(UserResponse {
|
||||
id: user.id,
|
||||
@@ -2846,7 +2846,7 @@ pub async fn get_recommendations(
|
||||
.bind(course_id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// 2. Obtener metadatos de la lección (títulos y etiquetas) para contexto
|
||||
#[derive(sqlx::FromRow)]
|
||||
@@ -2878,7 +2878,7 @@ pub async fn get_recommendations(
|
||||
.bind(course_id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// 3. Preparar contexto de IA con Análisis de Habilidades
|
||||
use std::collections::HashMap;
|
||||
@@ -3080,7 +3080,7 @@ pub async fn evaluate_audio_response(
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let ai_data: serde_json::Value = response.json().await.map_err(|e| {
|
||||
(
|
||||
@@ -3101,7 +3101,7 @@ pub async fn evaluate_audio_response(
|
||||
"feedback": "Lo siento, tuve un problema analizando tu respuesta. ¡Sigue practicando!"
|
||||
})
|
||||
})
|
||||
).map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Mapping failed: {}", e)))?;
|
||||
).map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(grading))
|
||||
}
|
||||
@@ -3152,7 +3152,7 @@ pub async fn evaluate_audio_file(
|
||||
audio_data = field
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.to_vec();
|
||||
tracing::info!("Received audio file: {} bytes", audio_data.len());
|
||||
}
|
||||
@@ -3322,7 +3322,7 @@ pub async fn evaluate_audio_file(
|
||||
"feedback": "Lo siento, tuve un problema analizando tu respuesta con Whisper. ¡Sigue practicando!"
|
||||
})
|
||||
})
|
||||
).map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Mapping failed: {}", e)))?;
|
||||
).map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
grading.transcript = Some(transcript.clone());
|
||||
|
||||
// 3. Guardar respuesta de audio en la base de datos
|
||||
@@ -4242,7 +4242,7 @@ pub async fn chat_with_tutor(
|
||||
.build()
|
||||
.map_err(|e| {
|
||||
tracing::warn!("Failed to create HTTP client for embeddings: {}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, format!("HTTP client error: {}", e))
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string())
|
||||
})?;
|
||||
|
||||
let ollama_url = ai::get_ollama_url();
|
||||
@@ -4666,11 +4666,11 @@ pub async fn chat_role_play(
|
||||
"temperature": 0.8
|
||||
}))
|
||||
.send().await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error en la solicitud de IA: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let err_body = response.text().await.unwrap_or_default();
|
||||
return Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Error de la API de IA: {}", err_body)));
|
||||
let _err_body = response.text().await.unwrap_or_default();
|
||||
return Err((StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()));
|
||||
}
|
||||
|
||||
let ai_data: serde_json::Value = response.json().await.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error al analizar la respuesta de la IA".into()))?;
|
||||
@@ -4714,7 +4714,7 @@ pub async fn get_lesson_feedback(
|
||||
.bind(lesson_id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.ok_or((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"No se encontró calificación para esta lección".into(),
|
||||
@@ -5197,7 +5197,7 @@ pub async fn update_lesson_collaborative_doc(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if !lesson_exists {
|
||||
return Err((StatusCode::NOT_FOUND, "Lección no encontrada".into()));
|
||||
@@ -5220,7 +5220,7 @@ pub async fn update_lesson_collaborative_doc(
|
||||
.bind(payload.base_revision)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.rows_affected();
|
||||
|
||||
if rows_updated == 1 {
|
||||
@@ -5241,7 +5241,7 @@ pub async fn update_lesson_collaborative_doc(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if existing.is_none() && payload.base_revision == 0 {
|
||||
// Primer guardado
|
||||
@@ -5258,7 +5258,7 @@ pub async fn update_lesson_collaborative_doc(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
@@ -5273,7 +5273,7 @@ pub async fn update_lesson_collaborative_doc(
|
||||
.bind(user_id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
return Ok(Json(UpdateCollaborativeDocResponse {
|
||||
lesson_id: id,
|
||||
@@ -5294,7 +5294,7 @@ pub async fn update_lesson_collaborative_doc(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let (sc, sr) = server.map(|r| (r.content, r.revision)).unwrap_or_default();
|
||||
|
||||
@@ -5428,7 +5428,7 @@ pub async fn list_lesson_annotations(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
Ok(Json(rows))
|
||||
}
|
||||
|
||||
@@ -5464,7 +5464,7 @@ pub async fn create_lesson_annotation(
|
||||
.bind(payload.position_data)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
Ok((StatusCode::CREATED, Json(row)))
|
||||
}
|
||||
|
||||
@@ -5511,7 +5511,7 @@ pub async fn delete_lesson_annotation(
|
||||
.bind(lesson_id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.rows_affected();
|
||||
if affected == 0 {
|
||||
Err((StatusCode::NOT_FOUND, "Anotación no encontrada".to_string()))
|
||||
@@ -5535,7 +5535,7 @@ pub async fn get_my_annotations(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
Ok(Json(rows))
|
||||
}
|
||||
|
||||
@@ -5595,7 +5595,7 @@ pub async fn assign_mentor(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
match role.as_deref() {
|
||||
Some("instructor") | Some("admin") => {}
|
||||
@@ -5620,7 +5620,7 @@ pub async fn assign_mentor(
|
||||
.bind(&payload.notes)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(row))
|
||||
}
|
||||
@@ -5654,7 +5654,7 @@ pub async fn list_course_mentorships(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(rows))
|
||||
}
|
||||
@@ -5673,7 +5673,7 @@ pub async fn delete_mentorship(
|
||||
.bind(org_ctx.id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.rows_affected();
|
||||
|
||||
if affected == 0 {
|
||||
@@ -5714,7 +5714,7 @@ pub async fn get_my_mentor(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(row))
|
||||
}
|
||||
@@ -5750,7 +5750,7 @@ pub async fn get_my_mentees(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(rows))
|
||||
}
|
||||
|
||||
@@ -283,7 +283,7 @@ pub async fn list_ai_audit_logs(
|
||||
.bind(offset)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al listar auditoría de IA: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let mut items = Vec::new();
|
||||
|
||||
@@ -371,7 +371,7 @@ pub async fn review_ai_audit_log(
|
||||
.bind(note_or_null)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al actualizar auditoría IA: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if updated.rows_affected() == 0 {
|
||||
return Err((StatusCode::NOT_FOUND, "Registro de auditoría no encontrado".to_string()));
|
||||
@@ -437,7 +437,7 @@ pub async fn get_ai_audit_metrics(
|
||||
.bind(days)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error métricas de auditoría: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let total_chat_logs: i64 = totals.get("total_chat");
|
||||
let total_reviewed: i64 = totals.get("reviewed");
|
||||
@@ -456,7 +456,7 @@ pub async fn get_ai_audit_metrics(
|
||||
.bind(days)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error escaneando logs: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let mut signal_counts: std::collections::HashMap<String, i64> = std::collections::HashMap::new();
|
||||
let mut total_flagged: i64 = 0;
|
||||
|
||||
@@ -49,7 +49,7 @@ async fn ensure_announcement_author_exists(
|
||||
.bind(user_id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if exists {
|
||||
return Ok(());
|
||||
@@ -71,7 +71,7 @@ async fn ensure_announcement_author_exists(
|
||||
.bind(normalized_role)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("No se pudo provisionar usuario LMS: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -97,7 +97,7 @@ pub async fn list_announcements(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Adjuntar cohort_ids a cada anuncio
|
||||
for a in &mut announcements {
|
||||
@@ -107,7 +107,7 @@ pub async fn list_announcements(
|
||||
.bind(a.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if !cohorts.is_empty() {
|
||||
a.cohort_ids = Some(cohorts.into_iter().map(|c| c.0).collect());
|
||||
@@ -136,7 +136,7 @@ pub async fn create_announcement(
|
||||
let mut tx = pool
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// 1. Crear anuncio
|
||||
let mut announcement = sqlx::query_as::<_, CourseAnnouncement>(
|
||||
@@ -152,7 +152,7 @@ pub async fn create_announcement(
|
||||
.bind(payload.is_pinned.unwrap_or(false))
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// 2. Vincular cohortes si se proporcionan
|
||||
if let Some(ref cohort_ids) = payload.cohort_ids {
|
||||
@@ -164,14 +164,14 @@ pub async fn create_announcement(
|
||||
.bind(cohort_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
}
|
||||
announcement.cohort_ids = Some(cohort_ids.clone());
|
||||
}
|
||||
|
||||
tx.commit()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// 3. Obtener estudiantes objetivo para notificaciones
|
||||
let enrolled_students = if let Some(ref cohort_ids) = payload.cohort_ids {
|
||||
@@ -207,7 +207,7 @@ pub async fn create_announcement(
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
}
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Crear notificación para cada estudiante inscrito
|
||||
for (student_id,) in enrolled_students {
|
||||
@@ -276,7 +276,7 @@ pub async fn update_announcement(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(announcement))
|
||||
}
|
||||
@@ -302,7 +302,7 @@ pub async fn delete_announcement(
|
||||
.bind(org_ctx.id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ pub async fn list_cohorts(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(cohorts))
|
||||
}
|
||||
@@ -43,7 +43,7 @@ pub async fn create_cohort(
|
||||
.bind(payload.description)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(cohort))
|
||||
}
|
||||
@@ -63,7 +63,7 @@ pub async fn add_cohort_member(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if !exists {
|
||||
return Err((StatusCode::NOT_FOUND, "Cohorte no encontrada".to_string()));
|
||||
@@ -81,7 +81,7 @@ pub async fn add_cohort_member(
|
||||
.bind(payload.user_id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(member))
|
||||
}
|
||||
@@ -100,7 +100,7 @@ pub async fn remove_cohort_member(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if !exists {
|
||||
return Err((StatusCode::NOT_FOUND, "Cohorte no encontrada".to_string()));
|
||||
@@ -111,7 +111,7 @@ pub async fn remove_cohort_member(
|
||||
.bind(user_id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
@@ -130,7 +130,7 @@ pub async fn get_cohort_members(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if !exists {
|
||||
return Err((StatusCode::NOT_FOUND, "Cohorte no encontrada".to_string()));
|
||||
@@ -140,7 +140,7 @@ pub async fn get_cohort_members(
|
||||
.bind(cohort_id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(members))
|
||||
}
|
||||
|
||||
@@ -454,7 +454,7 @@ pub async fn list_threads(
|
||||
let threads = sql_query
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(threads))
|
||||
}
|
||||
@@ -497,7 +497,7 @@ pub async fn create_thread(
|
||||
.bind(&payload.content)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Suscribir automáticamente al autor al hilo
|
||||
let _ = sqlx::query(
|
||||
@@ -654,7 +654,7 @@ fn get_thread_posts_recursive<'a>(
|
||||
let mut posts = sql_query
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Obtener respuestas recursivamente para cada mensaje
|
||||
for post in &mut posts {
|
||||
@@ -691,7 +691,7 @@ pub async fn pin_thread(
|
||||
.bind(org_ctx.id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
@@ -721,7 +721,7 @@ pub async fn lock_thread(
|
||||
.bind(org_ctx.id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
@@ -779,7 +779,7 @@ pub async fn create_post(
|
||||
.bind(payload.content)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Notificar a los suscritos al hilo (excepto autor de la respuesta)
|
||||
let mut recipients = sqlx::query_as::<_, (Uuid, String, Option<String>)>(
|
||||
@@ -883,7 +883,7 @@ pub async fn endorse_post(
|
||||
.bind(org_ctx.id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
@@ -912,7 +912,7 @@ pub async fn vote_post(
|
||||
.bind(&payload.vote_type)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Recalcular votos positivos
|
||||
let upvote_count: i64 = sqlx::query_scalar(
|
||||
@@ -928,7 +928,7 @@ pub async fn vote_post(
|
||||
.bind(post_id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
@@ -951,7 +951,7 @@ pub async fn subscribe_thread(
|
||||
.bind(claims.sub)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
@@ -971,7 +971,7 @@ pub async fn unsubscribe_thread(
|
||||
.bind(org_ctx.id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ pub async fn forgot_password(
|
||||
.bind(&email)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let Some(user) = user else {
|
||||
// Respuesta genérica — no revelar si el email existe
|
||||
@@ -210,7 +210,7 @@ pub async fn forgot_password(
|
||||
.bind(user.id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Insertar nuevo token (expira en 1 hora)
|
||||
sqlx::query(
|
||||
@@ -220,7 +220,7 @@ pub async fn forgot_password(
|
||||
.bind(&token)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Intentar enviar email (fire-and-forget en caso de error SMTP)
|
||||
let base_url = env::var("EXPERIENCE_URL").unwrap_or_else(|_| "https://openccb.local".to_string());
|
||||
@@ -283,7 +283,7 @@ pub async fn reset_password(
|
||||
.bind(&token)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let Some((user_id,)) = row else {
|
||||
return Err((
|
||||
@@ -303,14 +303,14 @@ pub async fn reset_password(
|
||||
.bind(user_id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Marcar token como usado
|
||||
sqlx::query("UPDATE password_reset_tokens SET used_at = NOW() WHERE token = $1")
|
||||
.bind(&token)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(MessageResponse {
|
||||
message: "Contraseña actualizada correctamente. Ya puedes iniciar sesión.".to_string(),
|
||||
|
||||
@@ -55,7 +55,7 @@ pub async fn generate_knowledge_embeddings(
|
||||
.danger_accept_invalid_certs(true)
|
||||
.danger_accept_invalid_hostnames(true)
|
||||
.build()
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("HTTP client error: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let ollama_url = ai::get_ollama_url();
|
||||
let model = ai::get_embedding_model();
|
||||
@@ -73,7 +73,7 @@ pub async fn generate_knowledge_embeddings(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let _total = entries.len();
|
||||
let mut processed = 0;
|
||||
@@ -144,7 +144,7 @@ pub async fn regenerate_knowledge_embedding(
|
||||
.danger_accept_invalid_certs(true)
|
||||
.danger_accept_invalid_hostnames(true)
|
||||
.build()
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("HTTP client error: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let ollama_url = ai::get_ollama_url();
|
||||
let model = ai::get_embedding_model();
|
||||
@@ -157,13 +157,13 @@ pub async fn regenerate_knowledge_embedding(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "Entrada de la base de conocimientos no encontrada".to_string()))?;
|
||||
|
||||
// Generar embedding
|
||||
let response = generate_embedding(&client, &ollama_url, &model, &entry.content_chunk)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("AI error: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let pgvector = ai::embedding_to_pgvector(&response.embedding);
|
||||
|
||||
@@ -180,7 +180,7 @@ pub async fn regenerate_knowledge_embedding(
|
||||
.bind(entry_id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
@@ -198,7 +198,7 @@ pub async fn semantic_search_knowledge(
|
||||
.danger_accept_invalid_certs(true)
|
||||
.danger_accept_invalid_hostnames(true)
|
||||
.build()
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("HTTP client error: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let ollama_url = ai::get_ollama_url();
|
||||
let model = ai::get_embedding_model();
|
||||
@@ -206,7 +206,7 @@ pub async fn semantic_search_knowledge(
|
||||
// Generar embedding para la consulta
|
||||
let embedding_response = generate_embedding(&client, &ollama_url, &model, &filters.query)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("AI error: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let pgvector = ai::embedding_to_pgvector(&embedding_response.embedding);
|
||||
|
||||
@@ -264,7 +264,7 @@ pub async fn semantic_search_knowledge(
|
||||
let results = sql_query
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(results))
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ pub async fn import_faq_candidates(
|
||||
.bind(limit)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al importar candidatos FAQ: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let imported: i64 = row.get("imported");
|
||||
let skipped: i64 = row.get("skipped");
|
||||
@@ -250,7 +250,7 @@ pub async fn list_faq_review_queue(
|
||||
.bind(offset)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al listar la cola FAQ: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let total: i64 = sqlx::query_scalar(
|
||||
r#"
|
||||
@@ -264,7 +264,7 @@ pub async fn list_faq_review_queue(
|
||||
.bind(filters.status.clone())
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al contar cola FAQ: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(FaqReviewQueueResponse {
|
||||
items,
|
||||
@@ -292,7 +292,7 @@ pub async fn answer_faq_review_item(
|
||||
let mut tx = pool
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("No se pudo iniciar transacción FAQ: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let queue_item: (String, String) = sqlx::query_as(
|
||||
"SELECT status, question_text FROM faq_review_queue WHERE id = $1 AND organization_id = $2"
|
||||
@@ -301,7 +301,7 @@ pub async fn answer_faq_review_item(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al buscar item FAQ: {}", e)))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "Item de revisión no encontrado".to_string()))?;
|
||||
|
||||
if queue_item.0 == "dismissed" {
|
||||
@@ -335,7 +335,7 @@ pub async fn answer_faq_review_item(
|
||||
.bind(claims.sub)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al crear FAQ: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
@@ -358,7 +358,7 @@ pub async fn answer_faq_review_item(
|
||||
.bind(org_ctx.id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al actualizar cola FAQ: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
} else {
|
||||
sqlx::query(
|
||||
r#"
|
||||
@@ -379,12 +379,12 @@ pub async fn answer_faq_review_item(
|
||||
.bind(org_ctx.id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al responder cola FAQ: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
}
|
||||
|
||||
tx.commit()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("No se pudo confirmar transacción FAQ: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
@@ -416,7 +416,7 @@ pub async fn dismiss_faq_review_item(
|
||||
.bind(org_ctx.id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al descartar item FAQ: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err((StatusCode::NOT_FOUND, "Item de revisión no encontrado".to_string()));
|
||||
@@ -475,7 +475,7 @@ pub async fn list_faq_entries(
|
||||
.bind(offset)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al listar FAQ: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(rows))
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ pub async fn list_course_lti_tools(
|
||||
.bind(course_id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let tools = rows
|
||||
.into_iter()
|
||||
@@ -139,7 +139,7 @@ pub async fn create_course_lti_tool(
|
||||
.bind(payload.config.unwrap_or(serde_json::json!({})))
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
@@ -206,7 +206,7 @@ pub async fn update_course_lti_tool(
|
||||
.bind(payload.config)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "Herramienta LTI no encontrada".to_string()))?;
|
||||
|
||||
Ok(Json(LtiExternalTool {
|
||||
@@ -236,7 +236,7 @@ pub async fn delete_course_lti_tool(
|
||||
.bind(course_id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if res.rows_affected() == 0 {
|
||||
return Err((StatusCode::NOT_FOUND, "Herramienta LTI no encontrada".to_string()));
|
||||
@@ -276,7 +276,7 @@ pub async fn lti_grade_passback(
|
||||
.bind(tool_id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "Herramienta LTI no encontrada".to_string()))?;
|
||||
|
||||
let organization_id: Uuid = tool_row.get("organization_id");
|
||||
@@ -323,7 +323,7 @@ pub async fn lti_grade_passback(
|
||||
.bind(organization_id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if !user_exists {
|
||||
return Err((StatusCode::UNPROCESSABLE_ENTITY, "user_id inválido para esta organización".to_string()));
|
||||
@@ -348,7 +348,7 @@ pub async fn lti_grade_passback(
|
||||
.bind(organization_id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if !lesson_ok {
|
||||
return Err((StatusCode::UNPROCESSABLE_ENTITY, "lesson_id no pertenece al curso".to_string()));
|
||||
@@ -384,7 +384,7 @@ pub async fn lti_grade_passback(
|
||||
.bind(payload.metadata.clone().unwrap_or(serde_json::json!({})))
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Sincronizar con gradebook solo cuando hay lesson_id
|
||||
if let Some(lesson_id) = payload.lesson_id {
|
||||
@@ -419,7 +419,7 @@ pub async fn lti_grade_passback(
|
||||
.bind(metadata)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
}
|
||||
|
||||
Ok(Json(LtiGradePassbackResponse {
|
||||
@@ -458,7 +458,7 @@ pub async fn rotate_lti_tool_secret(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if !tool_exists {
|
||||
return Err((StatusCode::NOT_FOUND, "Herramienta LTI no encontrada".to_string()));
|
||||
@@ -481,7 +481,7 @@ pub async fn rotate_lti_tool_secret(
|
||||
.bind(tool_id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
tracing::info!(
|
||||
"rotate_lti_tool_secret: rotated secret for tool {} in course {} org {}",
|
||||
@@ -634,7 +634,7 @@ pub async fn lti_ags_score_passback(
|
||||
.bind(tool_id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "Herramienta LTI no encontrada".to_string()))?;
|
||||
|
||||
let client_id = config.ags_client_id.as_deref().unwrap_or("");
|
||||
|
||||
@@ -20,7 +20,7 @@ pub async fn get_note(
|
||||
.bind(lesson_id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(note))
|
||||
}
|
||||
@@ -47,7 +47,7 @@ pub async fn save_note(
|
||||
.bind(payload.content)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(note))
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ pub async fn create_payment_preference(
|
||||
.bind(&course.currency)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// 3. Llamar a la API de Mercado Pago
|
||||
let mp_access_token = std::env::var("MP_ACCESS_TOKEN").unwrap_or_default();
|
||||
@@ -125,7 +125,7 @@ pub async fn create_payment_preference(
|
||||
.bind(transaction_id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(PaymentPreferenceResponse {
|
||||
preference_id,
|
||||
|
||||
@@ -88,7 +88,7 @@ pub async fn get_lesson_quality_metrics(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if enrolled == 0 {
|
||||
return Ok(Json(CourseQualityMetrics {
|
||||
@@ -146,7 +146,7 @@ pub async fn get_lesson_quality_metrics(
|
||||
.bind(enrolled)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let lessons = rows
|
||||
.into_iter()
|
||||
@@ -211,7 +211,7 @@ pub async fn get_quiz_discrimination_index(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if rows.is_empty() {
|
||||
return Ok(Json(CourseDiscriminationReport {
|
||||
@@ -300,7 +300,7 @@ pub async fn get_curricular_suggestions(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if enrolled < 5 {
|
||||
return Ok(Json(CurricularSuggestionsReport {
|
||||
@@ -335,7 +335,7 @@ pub async fn get_curricular_suggestions(
|
||||
.bind(enrolled)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let mut suggestions: Vec<CurricularSuggestion> = vec![];
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ pub async fn submit_assignment(
|
||||
.bind(lesson_id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if let Some(_) = existing {
|
||||
// Actualizar entrega existente
|
||||
@@ -98,7 +98,7 @@ pub async fn submit_assignment(
|
||||
.bind(lesson_id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
return Ok(Json(updated));
|
||||
}
|
||||
@@ -118,7 +118,7 @@ pub async fn submit_assignment(
|
||||
.bind(&payload.content)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(submission))
|
||||
}
|
||||
@@ -158,7 +158,7 @@ pub async fn get_peer_review_assignment(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(submission))
|
||||
}
|
||||
@@ -177,7 +177,7 @@ pub async fn submit_peer_review(
|
||||
.bind(payload.submission_id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let submission_user_id = match submission_row {
|
||||
Some(row) => row.get::<Uuid, _>("user_id"),
|
||||
@@ -199,7 +199,7 @@ pub async fn submit_peer_review(
|
||||
.bind(claims.sub)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if existing.is_some() {
|
||||
return Err((
|
||||
@@ -223,7 +223,7 @@ pub async fn submit_peer_review(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Recalcular nota final ponderada tras nueva revisión de par
|
||||
let lesson_id_for_calc: Uuid = sqlx::query_scalar(
|
||||
@@ -232,7 +232,7 @@ pub async fn submit_peer_review(
|
||||
.bind(payload.submission_id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let _ = recalculate_final_score(&pool, payload.submission_id, lesson_id_for_calc).await;
|
||||
|
||||
@@ -258,7 +258,7 @@ pub async fn get_my_submission_feedback(
|
||||
.bind(lesson_id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(reviews))
|
||||
}
|
||||
@@ -287,7 +287,7 @@ pub async fn list_lesson_submissions(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(submissions))
|
||||
}
|
||||
@@ -304,7 +304,7 @@ pub async fn get_submission_reviews(
|
||||
.bind(submission_id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(reviews))
|
||||
}
|
||||
@@ -325,7 +325,7 @@ pub async fn get_peer_review_settings(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(settings))
|
||||
}
|
||||
@@ -376,7 +376,7 @@ pub async fn upsert_peer_review_settings(
|
||||
.bind(auto_assign)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(settings))
|
||||
}
|
||||
@@ -400,7 +400,7 @@ pub async fn auto_assign_peer_reviews(
|
||||
.bind(lesson_id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.unwrap_or(2);
|
||||
|
||||
// Entregar todas las submissions de esta lección
|
||||
@@ -411,7 +411,7 @@ pub async fn auto_assign_peer_reviews(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let mut assignments_created: i64 = 0;
|
||||
|
||||
@@ -423,7 +423,7 @@ pub async fn auto_assign_peer_reviews(
|
||||
.bind(sub_id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let needed = (required as i64) - existing_count;
|
||||
if needed <= 0 {
|
||||
@@ -437,7 +437,7 @@ pub async fn auto_assign_peer_reviews(
|
||||
.bind(sub_id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Candidatos: otros alumnos que no sean el autor y no hayan revisado ya
|
||||
let candidates: Vec<Uuid> = submissions
|
||||
@@ -486,7 +486,7 @@ pub async fn auto_assign_peer_reviews(
|
||||
.bind(lesson_id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"lesson_id": lesson_id,
|
||||
@@ -517,7 +517,7 @@ pub async fn instructor_grade_submission(
|
||||
.bind(lesson_id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if sub_exists.is_none() {
|
||||
return Err((StatusCode::NOT_FOUND, "Entrega no encontrada".to_string()));
|
||||
@@ -542,7 +542,7 @@ pub async fn instructor_grade_submission(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// Recalcular nota final ponderada
|
||||
recalculate_final_score(&pool, payload.submission_id, lesson_id).await?;
|
||||
@@ -570,7 +570,7 @@ pub async fn get_my_submission(
|
||||
.bind(lesson_id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(sub))
|
||||
}
|
||||
@@ -594,7 +594,7 @@ async fn recalculate_final_score(
|
||||
.bind(submission_id)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.unwrap_or((70i32, 30i32, 2i32));
|
||||
|
||||
// Promedio de revisiones de pares (no instructor)
|
||||
@@ -604,7 +604,7 @@ async fn recalculate_final_score(
|
||||
.bind(submission_id)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.flatten();
|
||||
|
||||
// Calificación del instructor
|
||||
@@ -614,7 +614,7 @@ async fn recalculate_final_score(
|
||||
.bind(submission_id)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.flatten();
|
||||
|
||||
// Solo calcular nota final si hay suficientes revisiones de pares O hay nota del instructor
|
||||
@@ -624,7 +624,7 @@ async fn recalculate_final_score(
|
||||
.bind(submission_id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let has_enough_peers = peer_count >= required as i64;
|
||||
let final_score = match (peer_avg, instructor_score, has_enough_peers) {
|
||||
@@ -653,7 +653,7 @@ async fn recalculate_final_score(
|
||||
.bind(submission_id)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// También actualizar review_count en todas las submissions del mismo lesson
|
||||
let _ = sqlx::query(
|
||||
|
||||
@@ -74,7 +74,7 @@ pub async fn track_xapi_statement(
|
||||
.bind(payload.raw_statement)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(XapiStatementResponse {
|
||||
id: statement_id,
|
||||
|
||||
@@ -66,7 +66,7 @@ pub async fn global_search(
|
||||
.bind(limit)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
for (id, title, description) in courses {
|
||||
let snippet = description.map(|d| truncate(&d, 150));
|
||||
@@ -99,7 +99,7 @@ pub async fn global_search(
|
||||
.bind(limit)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
for (id, title, summary, course_id, course_title) in lessons {
|
||||
let snippet = summary.map(|s| truncate(&s, 150));
|
||||
|
||||
@@ -102,7 +102,7 @@ pub async fn list_course_study_rooms(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let rooms = rows
|
||||
.into_iter()
|
||||
@@ -212,7 +212,7 @@ pub async fn create_study_room(
|
||||
.bind(now)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
@@ -259,7 +259,7 @@ pub async fn join_study_room(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "Sala no encontrada".to_string()))?;
|
||||
|
||||
if room.status == "ended" {
|
||||
@@ -322,7 +322,7 @@ pub async fn end_study_room(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "Sala no encontrada".to_string()))?;
|
||||
|
||||
// Solo el creador puede terminar la sala
|
||||
@@ -351,7 +351,7 @@ pub async fn end_study_room(
|
||||
.bind(room_id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(EndStudyRoomResponse {
|
||||
room_id,
|
||||
@@ -373,7 +373,7 @@ pub async fn delete_study_room(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "Sala no encontrada".to_string()))?;
|
||||
|
||||
if claims.sub != created_by {
|
||||
@@ -384,7 +384,7 @@ pub async fn delete_study_room(
|
||||
.bind(room_id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
@@ -417,7 +417,7 @@ pub async fn get_study_room_recordings(
|
||||
.bind(org_ctx.id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.flatten()
|
||||
.ok_or((StatusCode::NOT_FOUND, "Sala no encontrada o sin ID BBB".to_string()))?;
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ pub async fn get_course_meetings(
|
||||
.bind(claims.org)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(meetings))
|
||||
}
|
||||
@@ -65,7 +65,7 @@ pub async fn create_meeting(
|
||||
.bind(join_url)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(meeting))
|
||||
}
|
||||
@@ -84,7 +84,7 @@ pub async fn delete_meeting(
|
||||
.bind(claims.org)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ pub async fn lti_login_initiation(
|
||||
.bind(¶ms.client_id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.ok_or((StatusCode::BAD_REQUEST, "Registro LTI no encontrado".to_string()))?;
|
||||
|
||||
// 2. Generar estado y nonce
|
||||
@@ -45,7 +45,7 @@ pub async fn lti_login_initiation(
|
||||
.bind(&nonce)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
// 4. Construir URL de redirección
|
||||
let mut url = format!(
|
||||
@@ -130,7 +130,7 @@ pub async fn lti_launch(
|
||||
.bind(aud)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "Registro LTI no encontrado para emisor/audiencia".to_string()))?;
|
||||
|
||||
// 3. Validar JWT
|
||||
@@ -143,7 +143,7 @@ pub async fn lti_launch(
|
||||
.bind(<i_claims.nonce)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.rows_affected() > 0;
|
||||
|
||||
if !nonce_exists {
|
||||
@@ -161,7 +161,7 @@ pub async fn lti_launch(
|
||||
.bind(registration.organization_id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
if user.is_none() {
|
||||
let new_user_id = Uuid::new_v4();
|
||||
@@ -182,7 +182,7 @@ pub async fn lti_launch(
|
||||
.bind(role)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
user = Some(User {
|
||||
id: new_user_id,
|
||||
@@ -211,7 +211,7 @@ pub async fn lti_launch(
|
||||
let studio_url = std::env::var("NEXT_PUBLIC_STUDIO_URL").unwrap_or_else(|_| "http://localhost:3001".to_string());
|
||||
|
||||
let token = common::auth::create_jwt(user.id, user.organization_id, &user.role)
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al crear el token: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
let redirect_target = lti_claims.resource_link.as_ref().map(|rl| rl.id.clone()).unwrap_or_default();
|
||||
|
||||
if lti_claims.message_type == "LtiDeepLinkingRequest" {
|
||||
@@ -228,7 +228,7 @@ pub async fn lti_launch(
|
||||
.bind(&settings.data)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Redirect::to(&format!("{}/lti/deep-linking?token={}&dl_token={}", studio_url, token, dl_request_id)))
|
||||
} else {
|
||||
@@ -258,7 +258,7 @@ pub async fn lti_deep_linking_response(
|
||||
.bind(dl_id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.ok_or((StatusCode::UNAUTHORIZED, "Solicitud de DL inválida o expirada".to_string()))?;
|
||||
|
||||
// Mapeo manual ya que no podemos usar query!/query_as! fácilmente para RETURNING sin una estructura
|
||||
@@ -274,7 +274,7 @@ pub async fn lti_deep_linking_response(
|
||||
.bind(registration_id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
let response_claims = common::models::LtiDeepLinkingResponseClaims {
|
||||
@@ -301,7 +301,7 @@ pub async fn lti_deep_linking_response(
|
||||
&response_claims,
|
||||
&private_key,
|
||||
)
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(json!({
|
||||
"jwt": response_jwt,
|
||||
|
||||
@@ -18,7 +18,7 @@ pub async fn get_public_profile(
|
||||
.bind(user_id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "Usuario no encontrado".to_string()))?;
|
||||
|
||||
let is_public: bool = user.get("is_public_profile");
|
||||
@@ -44,13 +44,13 @@ pub async fn get_public_profile(
|
||||
.bind(user_id)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let completed_courses: i64 = sqlx::query("SELECT COUNT(*) FROM enrollments WHERE user_id = $1 AND progress >= 100")
|
||||
.bind(user_id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
|
||||
.get(0);
|
||||
|
||||
Ok(Json(PublicProfile {
|
||||
@@ -87,7 +87,7 @@ pub async fn get_my_badges(
|
||||
.bind(claims.sub)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(Json(badges))
|
||||
}
|
||||
@@ -106,7 +106,7 @@ pub async fn award_badge(
|
||||
.bind(payload.badge_id)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
Ok(StatusCode::CREATED)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ pub async fn get_course_dropout_risks(
|
||||
}
|
||||
|
||||
calculate_risks_for_course(&pool, course_id, claims.org).await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
@@ -34,7 +34,7 @@ pub async fn get_course_dropout_risks(
|
||||
.bind(claims.org)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los riesgos: {}", e)))?;
|
||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
|
||||
|
||||
let risks: Vec<DropoutRisk> = rows.into_iter().map(|row| {
|
||||
DropoutRisk {
|
||||
|
||||
Generated
+686
-401
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@
|
||||
"isomorphic-dompurify": "^3.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.395.0",
|
||||
"mermaid": "^9.1.7",
|
||||
"mermaid": "^11.14.0",
|
||||
"next": "^14.2.35",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
|
||||
@@ -145,6 +145,7 @@ export default function LessonPlayerPage({ params }: { params: { id: string, les
|
||||
}
|
||||
return [...prev, res];
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`Failed to submit score for block ${blockId}`, err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,262 +323,3 @@ export default function PeerReviewPlayer({ courseId, lessonId, block }: PeerRevi
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
interface PeerReviewPlayerProps {
|
||||
courseId: string;
|
||||
lessonId: string;
|
||||
block: Block;
|
||||
}
|
||||
|
||||
export default function PeerReviewPlayer({ courseId, lessonId, block }: PeerReviewPlayerProps) {
|
||||
const [view, setView] = useState<'submit' | 'dashboard' | 'reviewing'>('submit');
|
||||
const [submissionContent, setSubmissionContent] = useState("");
|
||||
const [mySubmission, setMySubmission] = useState<CourseSubmission | null>(null);
|
||||
const [peerAssignment, setPeerAssignment] = useState<CourseSubmission | null>(null);
|
||||
const [feedbackReceived, setFeedbackReceived] = useState<PeerReview[]>([]);
|
||||
|
||||
// Review form state
|
||||
const [reviewScore, setReviewScore] = useState(80);
|
||||
const [reviewFeedback, setReviewFeedback] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
// Initial check - do we have a submission?
|
||||
// We don't have an endpoint for "get my submission" directly in the list I made,
|
||||
// but I can infer it or try to fetch feedback.
|
||||
// Actually, I missed adding "get my submission" endpoint.
|
||||
// Additionaly `getPeerReviewAssignment` excludes my own.
|
||||
// For now, let's assume if I can fetch feedback, I have submitted.
|
||||
// Or I can add a check endpoint.
|
||||
// Let's try to fetch feedback first.
|
||||
|
||||
useEffect(() => {
|
||||
const checkStatus = async () => {
|
||||
// For simplify, we just check feedback. If error or empty, maybe no submission?
|
||||
// Actually, `getMySubmissionFeedback` returns array.
|
||||
try {
|
||||
const reviews = await lmsApi.getMySubmissionFeedback(courseId, lessonId);
|
||||
setFeedbackReceived(reviews);
|
||||
// If we get reviews (or even empty array), it implies we might have submitted?
|
||||
// Not necessarily.
|
||||
// I should have added `getMySubmission`.
|
||||
// But for now, let's rely on local storage or just show "Submit" if no local state.
|
||||
// Ideally backend check.
|
||||
} catch (err) {
|
||||
// Error might mean not authorized or something.
|
||||
}
|
||||
};
|
||||
checkStatus();
|
||||
}, [courseId, lessonId]);
|
||||
|
||||
// Workaround: We will use a "saved" state in localStorage for "submitted" to avoid needing another endpoint right now,
|
||||
// or better: just try to submit. If it says "already submitted", handle it?
|
||||
// The backend `submit_assignment` UPDATES if exists. So it's safe to show form with previous content if we had it.
|
||||
// But we don't have "get my content".
|
||||
|
||||
// Let's add a "View my submission" button if I assume I submitted?
|
||||
// Maybe just show the dashboard if I have feedback.
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const sub = await lmsApi.submitAssignment(courseId, lessonId, submissionContent);
|
||||
setMySubmission(sub);
|
||||
setView('dashboard');
|
||||
setMessage("Submission saved successfully!");
|
||||
} catch (err: any) {
|
||||
setMessage("Failed to submit: " + err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartReview = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const assignment = await lmsApi.getPeerReviewAssignment(courseId, lessonId);
|
||||
if (!assignment) {
|
||||
setMessage("No assignments available for review at the moment. Please try again later.");
|
||||
} else {
|
||||
setPeerAssignment(assignment);
|
||||
setView('reviewing');
|
||||
setMessage("");
|
||||
}
|
||||
} catch (err: any) {
|
||||
setMessage("Error fetching assignment: " + err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitReview = async () => {
|
||||
if (!peerAssignment) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
await lmsApi.submitPeerReview(courseId, lessonId, peerAssignment.id, reviewScore, reviewFeedback);
|
||||
setMessage("Review submitted successfully! Thank you.");
|
||||
setPeerAssignment(null);
|
||||
setReviewFeedback("");
|
||||
setView('dashboard');
|
||||
} catch (err: any) {
|
||||
setMessage("Failed to submit review: " + err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (view === 'reviewing' && peerAssignment) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<button onClick={() => setView('dashboard')} className="text-sm text-gray-400 hover:text-white mb-4">
|
||||
← Back to Dashboard
|
||||
</button>
|
||||
<div className="p-6 bg-white/5 border border-white/10 rounded-2xl space-y-4">
|
||||
<h3 className="font-bold text-lg text-purple-400">Reviewing Peer Submission</h3>
|
||||
<div className="p-4 bg-black/30 rounded-xl text-gray-300 whitespace-pre-wrap">
|
||||
{peerAssignment.content}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-white/5 border border-white/10 rounded-2xl space-y-6">
|
||||
<h4 className="font-bold text-white">Your Feedback</h4>
|
||||
|
||||
{block.reviewCriteria && (
|
||||
<div className="text-sm text-gray-400 bg-blue-500/10 p-4 rounded-xl">
|
||||
<strong>Criteria:</strong> {block.reviewCriteria}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold uppercase text-gray-500 mb-2">Score (0-100)</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
value={reviewScore}
|
||||
onChange={(e) => setReviewScore(parseInt(e.target.value))}
|
||||
className="bg-black/20 border border-white/10 rounded-lg px-4 py-2 text-white w-24"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold uppercase text-gray-500 mb-2">Comments</label>
|
||||
<textarea
|
||||
value={reviewFeedback}
|
||||
onChange={(e) => setReviewFeedback(e.target.value)}
|
||||
className="w-full bg-black/20 border border-white/10 rounded-xl p-4 min-h-[120px] text-white focus:outline-none focus:border-purple-500"
|
||||
placeholder="Provide constructive feedback..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleSubmitReview}
|
||||
disabled={loading || !reviewFeedback}
|
||||
className="btn-primary w-full py-3 font-bold uppercase tracking-widest text-xs rounded-xl bg-purple-600 hover:bg-purple-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{loading ? "Submitting..." : "Submit Review"}
|
||||
</button>
|
||||
{message && <p className="text-center text-sm text-red-400">{message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (view === 'dashboard') {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="p-6 bg-green-500/10 border border-green-500/20 rounded-2xl flex items-center gap-4">
|
||||
<div className="text-2xl">✅</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-green-400">Work Submitted</h3>
|
||||
<p className="text-xs text-green-300/70">You can update your submission below or start reviewing peers.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
<h4 className="font-bold text-sm uppercase text-gray-500 tracking-widest">Actions</h4>
|
||||
<button
|
||||
onClick={handleStartReview}
|
||||
className="w-full p-6 bg-white/5 border border-white/10 rounded-2xl hover:bg-purple-500/10 hover:border-purple-500/30 transition-all text-left group"
|
||||
>
|
||||
<span className="text-2xl mb-2 block group-hover:scale-110 transition-transform">👀</span>
|
||||
<div className="font-bold text-purple-400">Review a Peer</div>
|
||||
<div className="text-xs text-gray-500 mt-1">Earn credit by reviewing other students' work.</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setView('submit')}
|
||||
className="w-full p-6 bg-white/5 border border-white/10 rounded-2xl hover:bg-blue-500/10 hover:border-blue-500/30 transition-all text-left"
|
||||
>
|
||||
<span className="text-2xl mb-2 block">📝</span>
|
||||
<div className="font-bold text-blue-400">Edit My Submission</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h4 className="font-bold text-sm uppercase text-gray-500 tracking-widest">Feedback Received</h4>
|
||||
{feedbackReceived.length === 0 ? (
|
||||
<div className="p-6 bg-white/5 border border-white/10 rounded-2xl text-center text-gray-500 italic text-sm">
|
||||
No reviews received yet. Check back later!
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{feedbackReceived.map(review => (
|
||||
<div key={review.id} className="p-4 bg-white/5 border border-white/10 rounded-xl space-y-2">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-xs font-bold text-gray-500">Peer Review</span>
|
||||
<span className="text-sm font-bold text-yellow-400">{review.score}/100</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-300">{review.feedback}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{message && <p className="text-center text-sm text-gray-400">{message}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Default: Submit View
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-bold flex items-center gap-2">
|
||||
<span className="text-purple-400">👥</span> {block.title || "Peer Assessment"}
|
||||
</h3>
|
||||
<div className="p-6 bg-white/5 border border-white/10 rounded-2xl whitespace-pre-wrap text-gray-300">
|
||||
{block.prompt || "Please submit your work below."}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<textarea
|
||||
value={submissionContent}
|
||||
onChange={(e) => setSubmissionContent(e.target.value)}
|
||||
className="w-full bg-black/20 border border-white/10 rounded-2xl p-6 min-h-[200px] text-white focus:outline-none focus:border-purple-500 transition-all"
|
||||
placeholder="Type your submission here or paste a link..."
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
{feedbackReceived.length > 0 && (
|
||||
<button onClick={() => setView('dashboard')} className="text-sm text-gray-500 hover:text-white">
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={loading || !submissionContent}
|
||||
className="btn-primary px-8 py-3 rounded-xl font-bold uppercase tracking-widest text-xs bg-blue-600 hover:bg-blue-700 transition-colors disabled:opacity-50 ml-auto"
|
||||
>
|
||||
{loading ? "Submitting..." : (mySubmission ? "Update Submission" : "Submit Assignment")}
|
||||
</button>
|
||||
</div>
|
||||
{message && <p className="text-center text-sm text-gray-400">{message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Generated
+745
-402
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@
|
||||
"framer-motion": "^11.2.10",
|
||||
"isomorphic-dompurify": "^3.10.0",
|
||||
"lucide-react": "^0.395.0",
|
||||
"mermaid": "^9.1.7",
|
||||
"mermaid": "^11.14.0",
|
||||
"next": "^14.2.35",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
|
||||
@@ -588,271 +588,3 @@ export default function PeerReviewDashboard() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default function PeerReviewDashboard() {
|
||||
const { id } = useParams() as { id: string };
|
||||
const router = useRouter();
|
||||
const [course, setCourse] = useState<Course | null>(null);
|
||||
const [lessons, setLessons] = useState<Lesson[]>([]);
|
||||
const [selectedLessonId, setSelectedLessonId] = useState<string | null>(null);
|
||||
const [submissions, setSubmissions] = useState<SubmissionWithReviews[]>([]);
|
||||
const [selectedSubmissionId, setSelectedSubmissionId] = useState<string | null>(null);
|
||||
const [reviews, setReviews] = useState<PeerReview[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [submissionsLoading, setSubmissionsLoading] = useState(false);
|
||||
const [reviewsLoading, setReviewsLoading] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const loadInitialData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const courseData = await cmsApi.getCourseWithFullOutline(id);
|
||||
setCourse(courseData);
|
||||
|
||||
const peerReviewLessons: Lesson[] = [];
|
||||
courseData.modules?.forEach(m => {
|
||||
m.lessons.forEach(l => {
|
||||
const hasPeerReview = l.metadata?.blocks?.some((b: any) => b.type === 'peer-review');
|
||||
if (hasPeerReview) {
|
||||
peerReviewLessons.push(l);
|
||||
}
|
||||
});
|
||||
});
|
||||
setLessons(peerReviewLessons);
|
||||
|
||||
if (peerReviewLessons.length > 0) {
|
||||
setSelectedLessonId(peerReviewLessons[0].id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading course data:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
loadInitialData();
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedLessonId) return;
|
||||
|
||||
const loadSubmissions = async () => {
|
||||
try {
|
||||
setSubmissionsLoading(true);
|
||||
const data = await lmsApi.listLessonSubmissions(id, selectedLessonId);
|
||||
setSubmissions(data);
|
||||
} catch (error) {
|
||||
console.error("Error loading submissions:", error);
|
||||
} finally {
|
||||
setSubmissionsLoading(false);
|
||||
}
|
||||
};
|
||||
loadSubmissions();
|
||||
}, [id, selectedLessonId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedSubmissionId) {
|
||||
setReviews([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const loadReviews = async () => {
|
||||
try {
|
||||
setReviewsLoading(true);
|
||||
const data = await lmsApi.getSubmissionReviews(selectedSubmissionId);
|
||||
setReviews(data);
|
||||
} catch (error) {
|
||||
console.error("Error loading reviews:", error);
|
||||
} finally {
|
||||
setReviewsLoading(false);
|
||||
}
|
||||
};
|
||||
loadReviews();
|
||||
}, [selectedSubmissionId]);
|
||||
|
||||
const filteredSubmissions = submissions.filter(s =>
|
||||
s.full_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
s.email.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-transparent flex items-center justify-center">
|
||||
<Loader2 className="w-10 h-10 text-blue-500 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-transparent text-gray-900 dark:text-white p-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<button onClick={() => router.back()} className="p-2 hover:bg-white/10 rounded-full transition-colors">
|
||||
<ArrowLeft className="w-6 h-6" />
|
||||
</button>
|
||||
<div>
|
||||
<h1 className="text-4xl font-black bg-gradient-to-r from-purple-600 to-blue-600 bg-clip-text text-transparent uppercase tracking-tighter">
|
||||
Peer Assessment
|
||||
</h1>
|
||||
<p className="text-slate-500 dark:text-gray-400 mt-1 font-medium">Monitor and manage student peer feedback loops</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CourseEditorLayout activeTab="peer-reviews">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-10">
|
||||
{/* Lessons List */}
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] px-4">Learning Activities</h3>
|
||||
<div className="space-y-2">
|
||||
{lessons.length === 0 ? (
|
||||
<div className="p-6 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl text-sm text-slate-400 italic">
|
||||
No peer review activities found.
|
||||
</div>
|
||||
) : (
|
||||
lessons.map(lesson => (
|
||||
<button
|
||||
key={lesson.id}
|
||||
onClick={() => {
|
||||
setSelectedLessonId(lesson.id);
|
||||
setSelectedSubmissionId(null);
|
||||
}}
|
||||
className={`w-full text-left p-5 rounded-[1.5rem] border transition-all active:scale-95 ${selectedLessonId === lesson.id
|
||||
? "bg-purple-50 dark:bg-purple-500/10 border-purple-200 dark:border-purple-500/50 text-purple-600 dark:text-purple-400 shadow-md shadow-purple-500/5 font-black uppercase tracking-tight"
|
||||
: "bg-white dark:bg-white/5 border-slate-200 dark:border-white/10 text-slate-500 dark:text-gray-400 hover:border-purple-500/30 font-bold"
|
||||
}`}
|
||||
>
|
||||
<div className="truncate text-sm">{lesson.title}</div>
|
||||
<div className="text-[9px] uppercase font-black tracking-widest opacity-60 mt-1">Requirement</div>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submissions List */}
|
||||
<div className="lg:col-span-3 space-y-8">
|
||||
<div className="bg-white dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-[2.5rem] p-10 shadow-sm">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-10">
|
||||
<h2 className="text-2xl font-black flex items-center gap-4 uppercase tracking-tight text-slate-900 dark:text-white">
|
||||
<div className="w-12 h-12 rounded-2xl bg-blue-50 dark:bg-blue-500/10 border border-blue-100 dark:border-blue-500/20 flex items-center justify-center text-blue-600 dark:text-blue-400 shadow-sm">
|
||||
<Users size={24} />
|
||||
</div>
|
||||
Submissions
|
||||
</h2>
|
||||
<div className="relative w-full md:w-80 group">
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-300 dark:text-gray-500 w-5 h-5 group-focus-within:text-blue-500 transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search student or email..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full bg-slate-50 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-[1.25rem] py-4 pl-12 pr-6 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/30 text-slate-900 dark:text-white transition-all shadow-inner"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{submissionsLoading ? (
|
||||
<div className="flex flex-col items-center justify-center py-32 space-y-4">
|
||||
<Loader2 className="w-12 h-12 text-blue-500 animate-spin" />
|
||||
<span className="text-xs font-black uppercase tracking-widest text-slate-400">Loading records...</span>
|
||||
</div>
|
||||
) : filteredSubmissions.length === 0 ? (
|
||||
<div className="text-center py-32 bg-slate-50 dark:bg-black/20 rounded-3xl border border-dashed border-slate-200 dark:border-white/10">
|
||||
<div className="w-20 h-20 bg-white dark:bg-white/5 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-sm">
|
||||
<MessageSquare className="w-10 h-10 text-slate-300 dark:text-gray-700" />
|
||||
</div>
|
||||
<p className="text-slate-500 dark:text-gray-500 font-bold uppercase tracking-tight">No submissions found for this activity.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4 overflow-y-auto max-h-[700px] pr-4 custom-scrollbar">
|
||||
{filteredSubmissions.map(sub => (
|
||||
<div key={sub.id} className="group">
|
||||
<div
|
||||
onClick={() => setSelectedSubmissionId(selectedSubmissionId === sub.id ? null : sub.id)}
|
||||
className={`p-6 rounded-[1.5rem] border transition-all cursor-pointer shadow-sm active:scale-[0.99] ${selectedSubmissionId === sub.id
|
||||
? "bg-blue-50 dark:bg-blue-500/5 border-blue-200 dark:border-blue-500/30"
|
||||
: "bg-slate-50/50 dark:bg-white/[0.02] border-slate-200 dark:border-white/5 hover:bg-white dark:hover:bg-white/[0.05] hover:border-blue-500/30"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-5">
|
||||
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-blue-600 to-indigo-700 flex items-center justify-center font-black text-white text-lg shadow-lg shadow-blue-500/20">
|
||||
{sub.full_name.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-black text-slate-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors uppercase tracking-tight text-sm">{sub.full_name}</div>
|
||||
<div className="text-[10px] font-bold text-slate-400 dark:text-gray-500 uppercase tracking-widest">{sub.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-10">
|
||||
<div className="text-right">
|
||||
<div className="text-lg font-black text-slate-900 dark:text-white flex items-center gap-2 justify-end">
|
||||
<Award className="w-5 h-5 text-yellow-500" />
|
||||
{sub.average_score !== null ? `${(sub.average_score).toFixed(1)}/10` : '—'}
|
||||
</div>
|
||||
<div className="text-[9px] text-slate-400 dark:text-gray-500 font-black uppercase tracking-[0.2em]">Rating</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className={`text-lg font-black flex items-center gap-2 justify-end ${sub.review_count >= 2 ? 'text-green-600' : 'text-orange-500'}`}>
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
{sub.review_count}
|
||||
</div>
|
||||
<div className="text-[9px] text-slate-400 dark:text-gray-500 font-black uppercase tracking-[0.2em]">Feedback</div>
|
||||
</div>
|
||||
<div className="text-right hidden xl:block">
|
||||
<div className="text-sm font-bold text-slate-400 dark:text-gray-500 flex items-center gap-2 justify-end">
|
||||
<Clock className="w-4 h-4" />
|
||||
{new Date(sub.submitted_at).toLocaleDateString()}
|
||||
</div>
|
||||
<div className="text-[9px] text-slate-400 dark:text-gray-500 font-black uppercase tracking-[0.2em]">Delivery</div>
|
||||
</div>
|
||||
<ChevronRight className={`w-6 h-6 text-slate-300 transition-all ${selectedSubmissionId === sub.id ? 'rotate-90 text-blue-500' : ''}`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reviews Detail Drawer-like expansion */}
|
||||
{selectedSubmissionId === sub.id && (
|
||||
<div className="mt-4 ml-8 p-10 bg-slate-50 dark:bg-black/40 border-l-4 border-blue-500 dark:border-blue-500/50 rounded-r-[2rem] space-y-8 animate-in slide-in-from-left-4 duration-300 shadow-inner">
|
||||
<h4 className="text-[10px] font-black uppercase tracking-[0.3em] text-blue-600 dark:text-blue-400 ml-1">Submission Feedback Details</h4>
|
||||
{reviewsLoading ? (
|
||||
<div className="flex py-10"><Loader2 className="w-8 h-8 animate-spin text-blue-500" /></div>
|
||||
) : reviews.length === 0 ? (
|
||||
<div className="p-8 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl text-center text-slate-400 italic">
|
||||
No peer reviews have been submitted for this user yet.
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{reviews.map(review => (
|
||||
<div key={review.id} className="p-6 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-2xl space-y-4 shadow-sm group/review hover:border-blue-500/30 transition-all">
|
||||
<div className="flex justify-between items-center pb-3 border-b border-slate-50 dark:border-white/5">
|
||||
<span className="text-[9px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest">Peer Evaluator</span>
|
||||
<span className="px-3 py-1 bg-yellow-50 dark:bg-yellow-500/10 text-yellow-600 dark:text-yellow-500 text-sm font-black rounded-lg">
|
||||
{review.score}/10
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 dark:text-gray-300 leading-relaxed italic font-medium px-2">
|
||||
"{review.feedback}"
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CourseEditorLayout>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user