add openapi
This commit is contained in:
parent
38debc2554
commit
bac09e5778
14 changed files with 697 additions and 66 deletions
243
Cargo.lock
generated
243
Cargo.lock
generated
|
@ -2,6 +2,12 @@
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adler"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ahash"
|
name = "ahash"
|
||||||
version = "0.7.6"
|
version = "0.7.6"
|
||||||
|
@ -24,6 +30,15 @@ dependencies = [
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "allocator-api2"
|
name = "allocator-api2"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
|
@ -289,6 +304,15 @@ version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484"
|
checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crc32fast"
|
||||||
|
version = "1.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-queue"
|
name = "crossbeam-queue"
|
||||||
version = "0.3.8"
|
version = "0.3.8"
|
||||||
|
@ -422,6 +446,16 @@ dependencies = [
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flate2"
|
||||||
|
version = "1.0.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743"
|
||||||
|
dependencies = [
|
||||||
|
"crc32fast",
|
||||||
|
"miniz_oxide",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
|
@ -687,6 +721,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"hashbrown 0.12.3",
|
"hashbrown 0.12.3",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -795,6 +830,8 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"url",
|
"url",
|
||||||
|
"utoipa",
|
||||||
|
"utoipa-swagger-ui",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -825,12 +862,31 @@ version = "0.3.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime_guess"
|
||||||
|
version = "2.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
|
||||||
|
dependencies = [
|
||||||
|
"mime",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minimal-lexical"
|
name = "minimal-lexical"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miniz_oxide"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
|
||||||
|
dependencies = [
|
||||||
|
"adler",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "0.8.8"
|
version = "0.8.8"
|
||||||
|
@ -1025,6 +1081,30 @@ version = "0.2.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-error"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-error-attr",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-error-attr"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.60"
|
version = "1.0.60"
|
||||||
|
@ -1115,6 +1195,35 @@ dependencies = [
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.16.20"
|
version = "0.16.20"
|
||||||
|
@ -1130,6 +1239,41 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-embed"
|
||||||
|
version = "6.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661"
|
||||||
|
dependencies = [
|
||||||
|
"rust-embed-impl",
|
||||||
|
"rust-embed-utils",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-embed-impl"
|
||||||
|
version = "6.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rust-embed-utils",
|
||||||
|
"shellexpand",
|
||||||
|
"syn 2.0.18",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-embed-utils"
|
||||||
|
version = "7.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74"
|
||||||
|
dependencies = [
|
||||||
|
"sha2",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.37.22"
|
version = "0.37.22"
|
||||||
|
@ -1177,6 +1321,15 @@ version = "1.0.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
|
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "same-file"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
@ -1285,6 +1438,15 @@ dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shellexpand"
|
||||||
|
version = "2.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4"
|
||||||
|
dependencies = [
|
||||||
|
"dirs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.1"
|
version = "1.4.1"
|
||||||
|
@ -1778,6 +1940,15 @@ dependencies = [
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicase"
|
||||||
|
version = "2.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
|
||||||
|
dependencies = [
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.13"
|
version = "0.3.13"
|
||||||
|
@ -1828,6 +1999,47 @@ dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utoipa"
|
||||||
|
version = "3.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68ae74ef183fae36d650f063ae7bde1cacbe1cd7e72b617cbe1e985551878b98"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"utoipa-gen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utoipa-gen"
|
||||||
|
version = "3.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7ea8ac818da7e746a63285594cce8a96f5e00ee31994e655bd827569cb8b137b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-error",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.18",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utoipa-swagger-ui"
|
||||||
|
version = "3.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4602d7100d3cfd8a086f30494e68532402ab662fa366c9d201d677e33cee138d"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"mime_guess",
|
||||||
|
"regex",
|
||||||
|
"rust-embed",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"utoipa",
|
||||||
|
"zip",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
@ -1851,6 +2063,16 @@ version = "0.9.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "walkdir"
|
||||||
|
version = "2.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
|
||||||
|
dependencies = [
|
||||||
|
"same-file",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "want"
|
name = "want"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
@ -1981,6 +2203,15 @@ version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-util"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-x86_64-pc-windows-gnu"
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
@ -2076,3 +2307,15 @@ name = "yansi"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zip"
|
||||||
|
version = "0.6.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"crc32fast",
|
||||||
|
"crossbeam-utils",
|
||||||
|
"flate2",
|
||||||
|
]
|
||||||
|
|
|
@ -24,6 +24,8 @@ thiserror = "1.0"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
tower-http = { version = "0.4", features = ["cors"] }
|
tower-http = { version = "0.4", features = ["cors"] }
|
||||||
dotenv_rs = "0.16"
|
dotenv_rs = "0.16"
|
||||||
|
utoipa = { version = "3", features = ["axum_extras", "uuid", "chrono", "preserve_order"] }
|
||||||
|
utoipa-swagger-ui = { version = "3", features = ["axum"]}
|
||||||
|
|
||||||
[profile.dev.package.sqlx-macros]
|
[profile.dev.package.sqlx-macros]
|
||||||
opt-level = 3
|
opt-level = 3
|
|
@ -1,13 +1,14 @@
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::FromRow;
|
use sqlx::FromRow;
|
||||||
|
use utoipa::ToSchema;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::Error;
|
use super::Error;
|
||||||
|
|
||||||
pub const DEFAULT_COLLECTIONS: [(&str, &str); 2] = [("blog", "Blog"), ("pages", "Pages")];
|
pub const DEFAULT_COLLECTIONS: [(&str, &str); 2] = [("blog", "Blog"), ("pages", "Pages")];
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, FromRow)]
|
#[derive(Debug, Deserialize, Serialize, FromRow, ToSchema)]
|
||||||
pub struct Collection {
|
pub struct Collection {
|
||||||
/// Collection ID
|
/// Collection ID
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
@ -25,7 +26,7 @@ pub struct Collection {
|
||||||
pub parent: Option<Uuid>,
|
pub parent: Option<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize, ToSchema)]
|
||||||
pub struct CollectionData {
|
pub struct CollectionData {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub slug: String,
|
pub slug: String,
|
||||||
|
|
|
@ -2,6 +2,7 @@ use async_trait::async_trait;
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{types::Json, FromRow};
|
use sqlx::{types::Json, FromRow};
|
||||||
|
use utoipa::ToSchema;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::cursor::{AsCursor, CursorList};
|
use crate::cursor::{AsCursor, CursorList};
|
||||||
|
@ -46,7 +47,7 @@ pub struct Post {
|
||||||
pub deleted_at: Option<NaiveDateTime>,
|
pub deleted_at: Option<NaiveDateTime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize, Debug, ToSchema)]
|
||||||
#[serde(tag = "kind")]
|
#[serde(tag = "kind")]
|
||||||
pub enum PostBlock {
|
pub enum PostBlock {
|
||||||
MarkupV1 {
|
MarkupV1 {
|
||||||
|
@ -63,7 +64,7 @@ pub enum PostBlock {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Picture inside a gallery
|
/// Picture inside a gallery
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize, Debug, ToSchema)]
|
||||||
pub struct ImageElement {
|
pub struct ImageElement {
|
||||||
/// URL of the picture
|
/// URL of the picture
|
||||||
pub url: String,
|
pub url: String,
|
||||||
|
@ -82,43 +83,90 @@ pub struct ImageElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Data to create a new post
|
/// Data to create a new post
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, ToSchema)]
|
||||||
pub struct CreatePostData {
|
pub struct CreatePostData {
|
||||||
|
/// URL-friendly short code for the post
|
||||||
pub slug: String,
|
pub slug: String,
|
||||||
|
|
||||||
|
/// Post title
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
|
||||||
|
/// Post description (for SEO/content)
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
|
||||||
|
/// Post tags (for internal search)
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
|
|
||||||
|
/// Post blocks (content)
|
||||||
pub blocks: Vec<PostBlock>,
|
pub blocks: Vec<PostBlock>,
|
||||||
|
|
||||||
|
/// Collections the post belongs to
|
||||||
pub collections: Vec<Uuid>,
|
pub collections: Vec<Uuid>,
|
||||||
|
|
||||||
|
/// Is the post published?
|
||||||
pub published: bool,
|
pub published: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Data to update a new post
|
/// Data to update a new post
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, ToSchema)]
|
||||||
pub struct UpdatePostData {
|
pub struct UpdatePostData {
|
||||||
|
/// URL-friendly short code for the post
|
||||||
pub slug: Option<String>,
|
pub slug: Option<String>,
|
||||||
|
|
||||||
|
/// Post title
|
||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
|
|
||||||
|
/// Post description (for SEO/content)
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
|
||||||
|
/// Post tags (for internal search)
|
||||||
pub tags: Option<Vec<String>>,
|
pub tags: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// Post blocks (content)
|
||||||
pub blocks: Option<Vec<PostBlock>>,
|
pub blocks: Option<Vec<PostBlock>>,
|
||||||
|
|
||||||
|
/// Collections the post belongs to
|
||||||
pub collections: Option<Vec<Uuid>>,
|
pub collections: Option<Vec<Uuid>>,
|
||||||
|
|
||||||
|
/// Is the post published?
|
||||||
pub published: Option<bool>,
|
pub published: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Post with site and author dereferenced for convenience
|
/// Post with site and author dereferenced for convenience
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, ToSchema)]
|
||||||
pub struct PostWithAuthor {
|
pub struct PostWithAuthor {
|
||||||
pub author_display_name: Option<String>,
|
// Author display name
|
||||||
|
pub author_display_name: String,
|
||||||
|
|
||||||
|
// Author username
|
||||||
pub author_username: String,
|
pub author_username: String,
|
||||||
pub slug: String,
|
|
||||||
pub title: String,
|
// Creation date
|
||||||
pub description: Option<String>,
|
|
||||||
pub tags: Vec<String>,
|
|
||||||
pub blocks: Json<Vec<PostBlock>>,
|
|
||||||
pub created_at: NaiveDateTime,
|
pub created_at: NaiveDateTime,
|
||||||
|
|
||||||
|
// Last modified time
|
||||||
pub modified_at: Option<NaiveDateTime>,
|
pub modified_at: Option<NaiveDateTime>,
|
||||||
pub published: bool,
|
|
||||||
|
/// URL-friendly short code for the post
|
||||||
|
pub slug: String,
|
||||||
|
|
||||||
|
/// Post title
|
||||||
|
pub title: String,
|
||||||
|
|
||||||
|
/// Post description (for SEO/content)
|
||||||
|
pub description: Option<String>,
|
||||||
|
|
||||||
|
/// Post tags (for internal search)
|
||||||
|
pub tags: Vec<String>,
|
||||||
|
|
||||||
|
/// Post blocks (content)
|
||||||
|
pub blocks: Vec<PostBlock>,
|
||||||
|
|
||||||
|
/// Collections the post belongs to
|
||||||
pub collections: Vec<Uuid>,
|
pub collections: Vec<Uuid>,
|
||||||
|
|
||||||
|
/// Is the post published?
|
||||||
|
pub published: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsCursor<NaiveDateTime> for PostWithAuthor {
|
impl AsCursor<NaiveDateTime> for PostWithAuthor {
|
||||||
|
|
|
@ -2,6 +2,7 @@ use async_trait::async_trait;
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::FromRow;
|
use sqlx::FromRow;
|
||||||
|
use utoipa::ToSchema;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::{collection::CollectionData, Error};
|
use super::{collection::CollectionData, Error};
|
||||||
|
@ -33,32 +34,57 @@ pub struct Site {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// More useful version of Site for showing to users
|
/// More useful version of Site for showing to users
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, ToSchema)]
|
||||||
pub struct SiteData {
|
pub struct SiteData {
|
||||||
|
/// Site name (unique per instance, shows up in URLs)
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
|
/// Site owner (user)
|
||||||
pub owner: String,
|
pub owner: String,
|
||||||
pub owner_display_name: Option<String>,
|
pub owner_display_name: Option<String>,
|
||||||
|
|
||||||
|
/// Site's displayed name
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
|
||||||
|
/// Site description (like a user's bio)
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
|
||||||
|
// Creation time
|
||||||
pub created_at: NaiveDateTime,
|
pub created_at: NaiveDateTime,
|
||||||
|
|
||||||
|
// Collections in the site
|
||||||
pub collections: Vec<CollectionData>,
|
pub collections: Vec<CollectionData>,
|
||||||
|
|
||||||
|
// Default collection (for homepage)
|
||||||
pub default_collection: Option<Uuid>,
|
pub default_collection: Option<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Data required to create a new site
|
/// Data required to create a new site
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, ToSchema)]
|
||||||
pub struct CreateSiteData {
|
pub struct CreateSiteData {
|
||||||
|
/// Site name (unique per instance, shows up in URLs)
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
|
/// Site's displayed name
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
|
||||||
|
/// Site description (like a user's bio)
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Data required to update a site's info
|
/// Data required to update a site's info
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, ToSchema)]
|
||||||
pub struct UpdateSiteData {
|
pub struct UpdateSiteData {
|
||||||
|
/// Site name (unique per instance, shows up in URLs)
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
|
|
||||||
|
/// Site's displayed name
|
||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
|
|
||||||
|
/// Site description (like a user's bio)
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
|
||||||
|
// Default collection (for homepage)
|
||||||
pub default_collection: Option<Uuid>,
|
pub default_collection: Option<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,8 +25,7 @@ impl PostRepository for Database {
|
||||||
from: Option<NaiveDateTime>,
|
from: Option<NaiveDateTime>,
|
||||||
limit: i64,
|
limit: i64,
|
||||||
) -> Result<CursorList<PostWithAuthor, NaiveDateTime>, Error> {
|
) -> Result<CursorList<PostWithAuthor, NaiveDateTime>, Error> {
|
||||||
let posts = sqlx::query_as!(
|
let posts = sqlx::query!(
|
||||||
PostWithAuthor,
|
|
||||||
r#"SELECT
|
r#"SELECT
|
||||||
COALESCE(users.display_name,users.name) as author_display_name,
|
COALESCE(users.display_name,users.name) as author_display_name,
|
||||||
users.name as author_username,
|
users.name as author_username,
|
||||||
|
@ -59,7 +58,27 @@ impl PostRepository for Database {
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(CursorList::new(posts, limit as usize))
|
Ok(CursorList::new(
|
||||||
|
posts
|
||||||
|
.into_iter()
|
||||||
|
.map(|record| PostWithAuthor {
|
||||||
|
author_display_name: record
|
||||||
|
.author_display_name
|
||||||
|
.unwrap_or_else(|| record.author_username.clone()),
|
||||||
|
author_username: record.author_username,
|
||||||
|
slug: record.slug,
|
||||||
|
title: record.title,
|
||||||
|
description: record.description,
|
||||||
|
tags: record.tags,
|
||||||
|
blocks: record.blocks.0,
|
||||||
|
created_at: record.created_at,
|
||||||
|
modified_at: record.modified_at,
|
||||||
|
published: record.published,
|
||||||
|
collections: record.collections,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
limit as usize,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_post_from_url(
|
async fn get_post_from_url(
|
||||||
|
@ -67,8 +86,7 @@ impl PostRepository for Database {
|
||||||
site: &str,
|
site: &str,
|
||||||
slug: &str,
|
slug: &str,
|
||||||
) -> Result<PostWithAuthor, content::Error> {
|
) -> Result<PostWithAuthor, content::Error> {
|
||||||
let post = sqlx::query_as!(
|
let post = sqlx::query!(
|
||||||
PostWithAuthor,
|
|
||||||
r#"SELECT
|
r#"SELECT
|
||||||
COALESCE(users.display_name,users.name) as author_display_name,
|
COALESCE(users.display_name,users.name) as author_display_name,
|
||||||
users.name as author_username,
|
users.name as author_username,
|
||||||
|
@ -100,7 +118,21 @@ impl PostRepository for Database {
|
||||||
sqlx::Error::RowNotFound => Error::NotFound,
|
sqlx::Error::RowNotFound => Error::NotFound,
|
||||||
_ => e.into(),
|
_ => e.into(),
|
||||||
})?;
|
})?;
|
||||||
Ok(post)
|
Ok(PostWithAuthor {
|
||||||
|
author_display_name: post
|
||||||
|
.author_display_name
|
||||||
|
.unwrap_or_else(|| post.author_username.clone()),
|
||||||
|
author_username: post.author_username,
|
||||||
|
slug: post.slug,
|
||||||
|
title: post.title,
|
||||||
|
description: post.description,
|
||||||
|
tags: post.tags,
|
||||||
|
blocks: post.blocks.0,
|
||||||
|
created_at: post.created_at,
|
||||||
|
modified_at: post.modified_at,
|
||||||
|
published: post.published,
|
||||||
|
collections: post.collections,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_post(
|
async fn create_post(
|
||||||
|
|
|
@ -2,9 +2,11 @@ use axum::http::StatusCode;
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::routing::post;
|
use axum::routing::post;
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
use axum::{extract::State, Router};
|
use axum::Router;
|
||||||
|
use serde::Serialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
use crate::auth::user::UserRepository;
|
use crate::auth::user::UserRepository;
|
||||||
use crate::database::Database;
|
use crate::database::Database;
|
||||||
|
@ -12,7 +14,24 @@ use crate::{
|
||||||
auth::hash::random, builtins::ROLE_SUPERADMIN, http::error::ApiError, state::AppState,
|
auth::hash::random, builtins::ROLE_SUPERADMIN, http::error::ApiError, state::AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
async fn bootstrap<Repo: UserRepository>(repository: Repo) -> impl IntoResponse {
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub(super) struct BootstrapInfo {
|
||||||
|
/// Admin username
|
||||||
|
pub username: String,
|
||||||
|
|
||||||
|
/// Admin password
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bootstrap the instance by creating a admin user
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/admin/bootstrap",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "List of collections fetched", body = [CollectionData])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub(super) async fn bootstrap<Repo: UserRepository>(repository: Repo) -> impl IntoResponse {
|
||||||
// Only allow this request if the user table is completely empty!
|
// Only allow this request if the user table is completely empty!
|
||||||
if !repository.has_no_users().await? {
|
if !repository.has_no_users().await? {
|
||||||
return Err(ApiError::Client {
|
return Err(ApiError::Client {
|
||||||
|
@ -22,16 +41,16 @@ async fn bootstrap<Repo: UserRepository>(repository: Repo) -> impl IntoResponse
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let username = "admin";
|
let username = "admin".to_string();
|
||||||
let password = random();
|
let password = random();
|
||||||
|
|
||||||
repository
|
repository
|
||||||
.create_user(username, &password, &[ROLE_SUPERADMIN].to_vec())
|
.create_user(&username, &password, &[ROLE_SUPERADMIN].to_vec())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Json(json!({"username": username, "password": password})))
|
Ok(Json(json!(BootstrapInfo { username, password })))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn router() -> Router<Arc<AppState>> {
|
pub(super) fn router() -> Router<Arc<AppState>> {
|
||||||
Router::new().route("/bootstrap", post(bootstrap::<Database>))
|
Router::new().route("/bootstrap", post(bootstrap::<Database>))
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,11 @@ use axum::{
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use chrono::Duration;
|
use chrono::{Duration, Utc};
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::{
|
auth::{
|
||||||
|
@ -25,13 +26,37 @@ use crate::{
|
||||||
state::AppState,
|
state::AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, ToSchema)]
|
||||||
struct LoginRequest {
|
pub(super) struct LoginRequest {
|
||||||
|
/// Account username
|
||||||
pub username: String,
|
pub username: String,
|
||||||
|
|
||||||
|
/// Account password
|
||||||
pub password: String,
|
pub password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn login<Repo: UserRepository + SessionRepository>(
|
#[derive(Serialize, ToSchema)]
|
||||||
|
pub(super) struct LoginResponse {
|
||||||
|
/// Session token
|
||||||
|
pub session_token: String,
|
||||||
|
|
||||||
|
/// Session expiration date
|
||||||
|
pub expires_at: chrono::DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sign in an account
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/auth/login",
|
||||||
|
request_body = LoginRequest,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Signed in successfully", body = LoginResponse)
|
||||||
|
),
|
||||||
|
security(
|
||||||
|
()
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub(super) async fn login<Repo: UserRepository + SessionRepository>(
|
||||||
repository: Repo,
|
repository: Repo,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
JsonBody(payload): JsonBody<LoginRequest>,
|
JsonBody(payload): JsonBody<LoginRequest>,
|
||||||
|
@ -61,9 +86,11 @@ async fn login<Repo: UserRepository + SessionRepository>(
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let token = session.token();
|
let token = session.token();
|
||||||
let mut response: Response =
|
let mut response: Response = Json(json!(LoginResponse {
|
||||||
Json(json!({ "session_token": token, "expires_at": session.expires_at.and_utc() }))
|
session_token: token,
|
||||||
.into_response();
|
expires_at: session.expires_at.and_utc()
|
||||||
|
}))
|
||||||
|
.into_response();
|
||||||
|
|
||||||
response.headers_mut().insert(
|
response.headers_mut().insert(
|
||||||
SET_COOKIE,
|
SET_COOKIE,
|
||||||
|
@ -76,11 +103,27 @@ async fn login<Repo: UserRepository + SessionRepository>(
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn me(RequireUser(user): RequireUser) -> Result<String, ApiError<'static>> {
|
/// Returns info about the logged-in user
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/auth/me",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "User info found", body = String)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub(super) async fn me(RequireUser(user): RequireUser) -> Result<String, ApiError<'static>> {
|
||||||
Ok(user.name)
|
Ok(user.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn logout<Repo: SessionRepository>(
|
/// Sign out and invalidate the session
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/auth/logout",
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Signed out successfully")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub(super) async fn logout<Repo: SessionRepository>(
|
||||||
repository: Repo,
|
repository: Repo,
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
RequireSession(session): RequireSession,
|
RequireSession(session): RequireSession,
|
||||||
|
@ -97,7 +140,7 @@ async fn logout<Repo: SessionRepository>(
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn router() -> Router<Arc<AppState>> {
|
pub(super) fn router() -> Router<Arc<AppState>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/login", post(login::<Database>))
|
.route("/login", post(login::<Database>))
|
||||||
.route("/logout", post(logout::<Database>))
|
.route("/logout", post(logout::<Database>))
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, Query},
|
extract::{Path, Query},
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
routing::{get, post},
|
routing::get,
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use chrono::NaiveDateTime;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
@ -16,17 +14,30 @@ use crate::{
|
||||||
state::AppState,
|
state::AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::pagination::PaginationQuery;
|
||||||
|
|
||||||
const DEFAULT_PAGE_SIZE: i64 = 10;
|
const DEFAULT_PAGE_SIZE: i64 = 10;
|
||||||
|
|
||||||
async fn create_collection<Repo: CollectionRepository>(repository: Repo) {
|
pub(super) async fn create_collection<Repo: CollectionRepository>(repository: Repo) {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_collection<Repo: CollectionRepository>(repository: Repo) {
|
pub(super) async fn get_collection<Repo: CollectionRepository>(repository: Repo) {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_collections_for_site<Repo: SiteRepository + CollectionRepository>(
|
/// List all collections in a site
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/collections/{site}",
|
||||||
|
params(
|
||||||
|
("site" = String, Path, description = "Site slug")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "List of collections fetched", body = [CollectionData])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub(super) async fn list_collections_for_site<Repo: SiteRepository + CollectionRepository>(
|
||||||
repository: Repo,
|
repository: Repo,
|
||||||
Path(site): Path<String>,
|
Path(site): Path<String>,
|
||||||
) -> Result<impl IntoResponse, ApiError<'static>> {
|
) -> Result<impl IntoResponse, ApiError<'static>> {
|
||||||
|
@ -35,21 +46,28 @@ async fn list_collections_for_site<Repo: SiteRepository + CollectionRepository>(
|
||||||
Ok(Json(repository.list_collections(&site_id).await?))
|
Ok(Json(repository.list_collections(&site_id).await?))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_collection<Repo: CollectionRepository>(repository: Repo) {
|
pub(super) async fn update_collection<Repo: CollectionRepository>(repository: Repo) {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_collection<Repo: CollectionRepository>(repository: Repo) {
|
pub(super) async fn delete_collection<Repo: CollectionRepository>(repository: Repo) {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Default)]
|
/// List posts within a collection (paginated)
|
||||||
struct PaginationQuery {
|
#[utoipa::path(
|
||||||
before: Option<NaiveDateTime>,
|
get,
|
||||||
limit: Option<i64>,
|
path = "/collections/{site}/{id}/posts",
|
||||||
}
|
params(
|
||||||
|
("site" = String, Path, description = "Site slug"),
|
||||||
async fn list_posts_in_collection<Repo: SiteRepository + PostRepository>(
|
("id" = Uuid, Path, description = "Collection ID"),
|
||||||
|
PaginationQuery
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "List of collections fetched", body = [CollectionData])
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub(super) async fn list_posts_in_collection<Repo: SiteRepository + PostRepository>(
|
||||||
repository: Repo,
|
repository: Repo,
|
||||||
Path((site, id)): Path<(String, Uuid)>,
|
Path((site, id)): Path<(String, Uuid)>,
|
||||||
query: Query<PaginationQuery>,
|
query: Query<PaginationQuery>,
|
||||||
|
@ -68,7 +86,7 @@ async fn list_posts_in_collection<Repo: SiteRepository + PostRepository>(
|
||||||
Ok(Json(results))
|
Ok(Json(results))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn router() -> Router<Arc<AppState>> {
|
pub(super) fn router() -> Router<Arc<AppState>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route(
|
.route(
|
||||||
"/:site",
|
"/:site",
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use utoipa::OpenApi;
|
||||||
|
use utoipa_swagger_ui::SwaggerUi;
|
||||||
|
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
use self::openapi::ApiDoc;
|
||||||
|
|
||||||
mod admin;
|
mod admin;
|
||||||
mod auth;
|
mod auth;
|
||||||
mod collections;
|
mod collections;
|
||||||
|
mod openapi;
|
||||||
|
mod pagination;
|
||||||
mod posts;
|
mod posts;
|
||||||
mod sites;
|
mod sites;
|
||||||
|
|
||||||
|
@ -16,4 +22,5 @@ pub fn create_router() -> Router<Arc<AppState>> {
|
||||||
.nest("/posts", posts::router())
|
.nest("/posts", posts::router())
|
||||||
.nest("/sites", sites::router())
|
.nest("/sites", sites::router())
|
||||||
.nest("/collections", collections::router())
|
.nest("/collections", collections::router())
|
||||||
|
.merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi()))
|
||||||
}
|
}
|
||||||
|
|
77
src/routes/openapi.rs
Normal file
77
src/routes/openapi.rs
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
use utoipa::{
|
||||||
|
openapi::security::{ApiKey, ApiKeyValue, SecurityScheme},
|
||||||
|
Modify, OpenApi,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::content::{
|
||||||
|
collection::CollectionData,
|
||||||
|
post::{CreatePostData, ImageElement, PostBlock, PostWithAuthor, UpdatePostData},
|
||||||
|
site::{CreateSiteData, SiteData, UpdateSiteData},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
admin::{self, BootstrapInfo},
|
||||||
|
auth, collections, posts, sites,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(OpenApi)]
|
||||||
|
#[openapi(
|
||||||
|
info(description = "Cenere content backend"),
|
||||||
|
paths(
|
||||||
|
// Authentication routes
|
||||||
|
auth::login,
|
||||||
|
auth::logout,
|
||||||
|
auth::me,
|
||||||
|
// Post routes
|
||||||
|
posts::get_post,
|
||||||
|
posts::create_post,
|
||||||
|
posts::update_post,
|
||||||
|
posts::delete_post,
|
||||||
|
// Site routes
|
||||||
|
sites::get_site,
|
||||||
|
sites::create_site,
|
||||||
|
sites::update_site,
|
||||||
|
sites::delete_site,
|
||||||
|
// Collection routes
|
||||||
|
collections::list_collections_for_site,
|
||||||
|
collections::list_posts_in_collection,
|
||||||
|
// Admin routes
|
||||||
|
admin::bootstrap
|
||||||
|
),
|
||||||
|
components(
|
||||||
|
schemas(
|
||||||
|
// Post data
|
||||||
|
PostWithAuthor,PostBlock,ImageElement,
|
||||||
|
CreatePostData,UpdatePostData,
|
||||||
|
// Site data
|
||||||
|
SiteData, CreateSiteData, UpdateSiteData,
|
||||||
|
// Collection data
|
||||||
|
CollectionData,
|
||||||
|
// Admin data
|
||||||
|
BootstrapInfo
|
||||||
|
)
|
||||||
|
),
|
||||||
|
modifiers(&SessionAddon),
|
||||||
|
tags(
|
||||||
|
(name = "posts", description = "Operation on posts within a site"),
|
||||||
|
(name = "sites", description = "Operation on sites"),
|
||||||
|
(name = "auth", description = "Session management")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub struct ApiDoc;
|
||||||
|
|
||||||
|
struct SessionAddon;
|
||||||
|
|
||||||
|
impl Modify for SessionAddon {
|
||||||
|
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
|
||||||
|
if let Some(components) = openapi.components.as_mut() {
|
||||||
|
components.add_security_scheme(
|
||||||
|
"session",
|
||||||
|
SecurityScheme::ApiKey(ApiKey::Cookie(ApiKeyValue::with_description(
|
||||||
|
"session",
|
||||||
|
"Session ID obtained via login or other authenticated calls",
|
||||||
|
))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
src/routes/pagination.rs
Normal file
12
src/routes/pagination.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Default, IntoParams)]
|
||||||
|
pub(super) struct PaginationQuery {
|
||||||
|
/// Cursor position (date-based)
|
||||||
|
pub before: Option<NaiveDateTime>,
|
||||||
|
|
||||||
|
/// How many items to return at most
|
||||||
|
pub limit: Option<i64>,
|
||||||
|
}
|
|
@ -22,7 +22,20 @@ use crate::{
|
||||||
state::AppState,
|
state::AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
async fn get_post<Repo: PostRepository>(
|
/// Retrieve post by site and post slug
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/posts/{site}/{slug}",
|
||||||
|
params(
|
||||||
|
("site" = String, Path, description = "Site slug"),
|
||||||
|
("slug" = String, Path, description = "Post slug")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Post found", body = PostWithAuthor),
|
||||||
|
(status = 404, description = "Post not found")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub(super) async fn get_post<Repo: PostRepository>(
|
||||||
repository: Repo,
|
repository: Repo,
|
||||||
OptionalUser(user): OptionalUser,
|
OptionalUser(user): OptionalUser,
|
||||||
Path((site, slug)): Path<(String, String)>,
|
Path((site, slug)): Path<(String, String)>,
|
||||||
|
@ -46,7 +59,21 @@ async fn get_post<Repo: PostRepository>(
|
||||||
Err(Error::NotFound.into())
|
Err(Error::NotFound.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_post<Repo: PostRepository + SiteRepository>(
|
/// Create a new post
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/posts/{site}",
|
||||||
|
params(
|
||||||
|
("site" = String, Path, description = "Site slug"),
|
||||||
|
),
|
||||||
|
request_body = CreatePostData,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Post created successfully"),
|
||||||
|
(status = 403, description = "You don't have the necessary permissions to create posts on this site"),
|
||||||
|
(status = 409, description = "The chosen post identifier/slug is not available")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub(super) async fn create_post<Repo: PostRepository + SiteRepository>(
|
||||||
repository: Repo,
|
repository: Repo,
|
||||||
Path(site): Path<String>,
|
Path(site): Path<String>,
|
||||||
RequireUser(user): RequireUser,
|
RequireUser(user): RequireUser,
|
||||||
|
@ -63,7 +90,22 @@ async fn create_post<Repo: PostRepository + SiteRepository>(
|
||||||
Ok(Json(json!({ "ok": true })))
|
Ok(Json(json!({ "ok": true })))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_post<Repo: PostRepository + SiteRepository>(
|
/// Update an existing post
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
path = "/posts/{site}/{slug}",
|
||||||
|
params(
|
||||||
|
("site" = String, Path, description = "Site slug"),
|
||||||
|
("slug" = String, Path, description = "Post slug")
|
||||||
|
),
|
||||||
|
request_body = UpdatePostData,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Post updated successfully"),
|
||||||
|
(status = 403, description = "You don't have the necessary permissions to update posts on this site"),
|
||||||
|
(status = 404, description = "Post not found")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub(super) async fn update_post<Repo: PostRepository + SiteRepository>(
|
||||||
repository: Repo,
|
repository: Repo,
|
||||||
Path((site, slug)): Path<(String, String)>,
|
Path((site, slug)): Path<(String, String)>,
|
||||||
RequireUser(user): RequireUser,
|
RequireUser(user): RequireUser,
|
||||||
|
@ -82,7 +124,21 @@ async fn update_post<Repo: PostRepository + SiteRepository>(
|
||||||
Ok(Json(json!({ "ok": true })))
|
Ok(Json(json!({ "ok": true })))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_post<Repo: PostRepository + SiteRepository>(
|
/// Delete an existing post
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/posts/{site}/{slug}",
|
||||||
|
params(
|
||||||
|
("site" = String, Path, description = "Site slug"),
|
||||||
|
("slug" = String, Path, description = "Post slug")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Post deleted successfully"),
|
||||||
|
(status = 403, description = "You don't have the necessary permissions to delete posts on this site"),
|
||||||
|
(status = 404, description = "Post not found")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub(super) async fn delete_post<Repo: PostRepository + SiteRepository>(
|
||||||
repository: Repo,
|
repository: Repo,
|
||||||
Path((site, slug)): Path<(String, String)>,
|
Path((site, slug)): Path<(String, String)>,
|
||||||
RequireUser(user): RequireUser,
|
RequireUser(user): RequireUser,
|
||||||
|
@ -100,7 +156,7 @@ async fn delete_post<Repo: PostRepository + SiteRepository>(
|
||||||
Ok(Json(json!({ "ok": true })))
|
Ok(Json(json!({ "ok": true })))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn router() -> Router<Arc<AppState>> {
|
pub(super) fn router() -> Router<Arc<AppState>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/:site", post(create_post::<Database>))
|
.route("/:site", post(create_post::<Database>))
|
||||||
.route(
|
.route(
|
||||||
|
|
|
@ -17,14 +17,36 @@ use crate::{
|
||||||
state::AppState,
|
state::AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
async fn get_site<Repo: SiteRepository>(
|
/// Retrieve site info/metadata
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/sites/{site}",
|
||||||
|
params(
|
||||||
|
("site" = String, Path, description = "Site slug")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Site info found", body = SiteData),
|
||||||
|
(status = 404, description = "Site not found")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub(super) async fn get_site<Repo: SiteRepository>(
|
||||||
repository: Repo,
|
repository: Repo,
|
||||||
Path(name): Path<String>,
|
Path(name): Path<String>,
|
||||||
) -> Result<impl IntoResponse, ApiError<'static>> {
|
) -> Result<impl IntoResponse, ApiError<'static>> {
|
||||||
Ok(Json(repository.get_site_by_name(&name).await?))
|
Ok(Json(repository.get_site_by_name(&name).await?))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_site<Repo: SiteRepository + CollectionRepository>(
|
/// Create a new site
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/sites",
|
||||||
|
request_body = CreateSiteData,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Site created"),
|
||||||
|
(status = 409, description = "The chosen site identifier/slug is not available")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub(super) async fn create_site<Repo: SiteRepository + CollectionRepository>(
|
||||||
repository: Repo,
|
repository: Repo,
|
||||||
RequireUser(user): RequireUser,
|
RequireUser(user): RequireUser,
|
||||||
JsonBody(options): JsonBody<CreateSiteData>,
|
JsonBody(options): JsonBody<CreateSiteData>,
|
||||||
|
@ -37,7 +59,20 @@ async fn create_site<Repo: SiteRepository + CollectionRepository>(
|
||||||
Ok(Json(json!({"ok": true})))
|
Ok(Json(json!({"ok": true})))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_site<Repo: SiteRepository>(
|
/// Update an existing site's info
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
path = "/sites/{site}",
|
||||||
|
params(
|
||||||
|
("site" = String, Path, description = "Site slug")
|
||||||
|
),
|
||||||
|
request_body = UpdateSiteData,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Site updated"),
|
||||||
|
(status = 404, description = "Site not found")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub(super) async fn update_site<Repo: SiteRepository>(
|
||||||
repository: Repo,
|
repository: Repo,
|
||||||
Path(name): Path<String>,
|
Path(name): Path<String>,
|
||||||
RequireUser(user): RequireUser,
|
RequireUser(user): RequireUser,
|
||||||
|
@ -47,7 +82,19 @@ async fn update_site<Repo: SiteRepository>(
|
||||||
Ok(Json(json!({"ok": true})))
|
Ok(Json(json!({"ok": true})))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_site<Repo: SiteRepository>(
|
/// Delete an existing site
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/sites/{site}",
|
||||||
|
params(
|
||||||
|
("site" = String, Path, description = "Site slug")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Site deleted"),
|
||||||
|
(status = 404, description = "Site not found")
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
pub(super) async fn delete_site<Repo: SiteRepository>(
|
||||||
repository: Repo,
|
repository: Repo,
|
||||||
Path(name): Path<String>,
|
Path(name): Path<String>,
|
||||||
RequireUser(user): RequireUser,
|
RequireUser(user): RequireUser,
|
||||||
|
@ -56,7 +103,7 @@ async fn delete_site<Repo: SiteRepository>(
|
||||||
Ok(Json(json!({"ok": true})))
|
Ok(Json(json!({"ok": true})))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn router() -> Router<Arc<AppState>> {
|
pub(super) fn router() -> Router<Arc<AppState>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", post(create_site::<Database>))
|
.route("/", post(create_site::<Database>))
|
||||||
.route(
|
.route(
|
||||||
|
|
Loading…
Reference in a new issue