feat(backend): add swagger-ui endpoint via utoipa
This commit is contained in:
171
backend/Cargo.lock
generated
171
backend/Cargo.lock
generated
@@ -2,6 +2,12 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adler2"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.4"
|
version = "1.1.4"
|
||||||
@@ -32,6 +38,15 @@ version = "1.0.102"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arbitrary"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||||
|
dependencies = [
|
||||||
|
"derive_arbitrary",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "argon2"
|
name = "argon2"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
@@ -205,6 +220,8 @@ dependencies = [
|
|||||||
"tower_governor",
|
"tower_governor",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"utoipa",
|
||||||
|
"utoipa-swagger-ui",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -424,6 +441,15 @@ version = "2.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
|
checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crc32fast"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-queue"
|
name = "crossbeam-queue"
|
||||||
version = "0.3.12"
|
version = "0.3.12"
|
||||||
@@ -499,6 +525,17 @@ dependencies = [
|
|||||||
"powerfmt",
|
"powerfmt",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_arbitrary"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "diff"
|
name = "diff"
|
||||||
version = "0.1.13"
|
version = "0.1.13"
|
||||||
@@ -672,6 +709,16 @@ version = "0.1.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flate2"
|
||||||
|
version = "1.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||||
|
dependencies = [
|
||||||
|
"miniz_oxide",
|
||||||
|
"zlib-rs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flume"
|
name = "flume"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@@ -1538,6 +1585,16 @@ dependencies = [
|
|||||||
"unicase",
|
"unicase",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miniz_oxide"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||||
|
dependencies = [
|
||||||
|
"adler2",
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -2333,6 +2390,40 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-embed"
|
||||||
|
version = "8.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27"
|
||||||
|
dependencies = [
|
||||||
|
"rust-embed-impl",
|
||||||
|
"rust-embed-utils",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-embed-impl"
|
||||||
|
version = "8.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rust-embed-utils",
|
||||||
|
"syn",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-embed-utils"
|
||||||
|
version = "8.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1"
|
||||||
|
dependencies = [
|
||||||
|
"sha2",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-multipart-rfc7578_2"
|
name = "rust-multipart-rfc7578_2"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -2681,6 +2772,12 @@ dependencies = [
|
|||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simd-adler32"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "simd_cesu8"
|
name = "simd_cesu8"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -3424,6 +3521,48 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utoipa"
|
||||||
|
version = "5.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8bde15df68e80b16c7d16b9616e80770ad158988daa56a27dccd1e55558b0160"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"utoipa-gen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utoipa-gen"
|
||||||
|
version = "5.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ba0b99ee52df3028635d93840c797102da61f8a7bb3cf751032455895b52ef8"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"regex",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utoipa-swagger-ui"
|
||||||
|
version = "9.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d047458f1b5b65237c2f6dc6db136945667f40a7668627b3490b9513a3d43a55"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"base64",
|
||||||
|
"mime_guess",
|
||||||
|
"regex",
|
||||||
|
"rust-embed",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"url",
|
||||||
|
"utoipa",
|
||||||
|
"zip",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.23.1"
|
version = "1.23.1"
|
||||||
@@ -4113,8 +4252,40 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zip"
|
||||||
|
version = "3.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308"
|
||||||
|
dependencies = [
|
||||||
|
"arbitrary",
|
||||||
|
"crc32fast",
|
||||||
|
"flate2",
|
||||||
|
"indexmap",
|
||||||
|
"memchr",
|
||||||
|
"zopfli",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zlib-rs"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.21"
|
version = "1.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zopfli"
|
||||||
|
version = "0.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"crc32fast",
|
||||||
|
"log",
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ rand = "0.10.1"
|
|||||||
futures-util = "0.3.32"
|
futures-util = "0.3.32"
|
||||||
mime_guess = "2.0.5"
|
mime_guess = "2.0.5"
|
||||||
tower_governor = { version = "0.8.0", default-features = false, features = ["axum", "tracing"] }
|
tower_governor = { version = "0.8.0", default-features = false, features = ["axum", "tracing"] }
|
||||||
|
utoipa = { version = "5.5.0", features = ["axum_extras", "chrono"] }
|
||||||
|
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
axum-test = "20.0.0"
|
axum-test = "20.0.0"
|
||||||
|
|||||||
@@ -24,14 +24,16 @@ use tower_http::{
|
|||||||
};
|
};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
use utoipa::OpenApi;
|
||||||
|
use utoipa_swagger_ui::SwaggerUi;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
model::{FileRepository, UserRepository},
|
model::{FileRepository, UserRepository},
|
||||||
web::{
|
web::{
|
||||||
mw_auth::{mw_ctx_resolver, mw_require_auth},
|
mw_auth::{mw_ctx_resolver, mw_require_auth},
|
||||||
routes_file::routes_file,
|
routes_file::{FileApi, routes_file},
|
||||||
routes_health::routes_health,
|
routes_health::{HealthApi, routes_health},
|
||||||
routes_login::routes_auth,
|
routes_login::{AuthApi, routes_auth},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -84,10 +86,15 @@ async fn main() -> Result<()> {
|
|||||||
routes_auth = routes_auth.layer(GovernorLayer::new(governor_conf_auth));
|
routes_auth = routes_auth.layer(GovernorLayer::new(governor_conf_auth));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut openapi = AuthApi::openapi();
|
||||||
|
openapi.merge(FileApi::openapi());
|
||||||
|
openapi.merge(HealthApi::openapi());
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.nest("/api", routes_file)
|
.nest("/api", routes_file)
|
||||||
.nest("/api/auth", routes_auth)
|
.nest("/api/auth", routes_auth)
|
||||||
.merge(routes_health())
|
.merge(routes_health())
|
||||||
|
.merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", openapi))
|
||||||
.layer(TraceLayer::new_for_http())
|
.layer(TraceLayer::new_for_http())
|
||||||
.layer(middleware::from_fn_with_state(
|
.layer(middleware::from_fn_with_state(
|
||||||
user_repository,
|
user_repository,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use sqlx::{PgPool, prelude::FromRow};
|
|||||||
|
|
||||||
use crate::error::{LoftError, Result};
|
use crate::error::{LoftError, Result};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, FromRow)]
|
#[derive(Clone, Debug, Serialize, FromRow, utoipa::ToSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct FileRecord {
|
pub struct FileRecord {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use axum::{
|
|||||||
routing::get,
|
routing::get,
|
||||||
};
|
};
|
||||||
use sqlx::types::uuid;
|
use sqlx::types::uuid;
|
||||||
|
use utoipa::OpenApi;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ctx::Ctx,
|
ctx::Ctx,
|
||||||
@@ -14,6 +15,20 @@ use crate::{
|
|||||||
model::{FileRecord, FileRepository},
|
model::{FileRecord, FileRepository},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(OpenApi)]
|
||||||
|
#[openapi(
|
||||||
|
paths(
|
||||||
|
list_files,
|
||||||
|
upload_file,
|
||||||
|
get_file,
|
||||||
|
delete_file,
|
||||||
|
download_file,
|
||||||
|
stream_part
|
||||||
|
),
|
||||||
|
components(schemas(FileRecord))
|
||||||
|
)]
|
||||||
|
pub struct FileApi;
|
||||||
|
|
||||||
pub fn routes_file(file_repository: FileRepository) -> Router {
|
pub fn routes_file(file_repository: FileRepository) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/files", get(list_files).post(upload_file))
|
.route("/files", get(list_files).post(upload_file))
|
||||||
@@ -23,11 +38,25 @@ pub fn routes_file(file_repository: FileRepository) -> Router {
|
|||||||
.with_state(file_repository)
|
.with_state(file_repository)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Upload a file
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/files",
|
||||||
|
tag = "files",
|
||||||
|
security(("cookie_auth" = [])),
|
||||||
|
request_body(content_type = "multipart/form-data", description = "Multipart form with a `file` field"),
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Uploaded file metadata", body = FileRecord),
|
||||||
|
(status = 400, description = "No file provided or malformed upload"),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 500, description = "Internal server error")
|
||||||
|
)
|
||||||
|
)]
|
||||||
async fn upload_file(
|
async fn upload_file(
|
||||||
State(file_repository): State<FileRepository>,
|
State(file_repository): State<FileRepository>,
|
||||||
ctx: Ctx,
|
ctx: Ctx,
|
||||||
mut multipart: Multipart,
|
mut multipart: Multipart,
|
||||||
) -> Result<Json<FileRecord>> {
|
) -> Result<(StatusCode, Json<FileRecord>)> {
|
||||||
let mut uploaded: Option<(String, String, usize)> = None;
|
let mut uploaded: Option<(String, String, usize)> = None;
|
||||||
|
|
||||||
while let Some(field) = multipart.next_field().await? {
|
while let Some(field) = multipart.next_field().await? {
|
||||||
@@ -45,9 +74,23 @@ async fn upload_file(
|
|||||||
.create_file_record(ctx.user_id(), &name, size, &key)
|
.create_file_record(ctx.user_id(), &name, size, &key)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Json(file_record))
|
Ok((StatusCode::CREATED, Json(file_record)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a file's metadata
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/files/{id}",
|
||||||
|
tag = "files",
|
||||||
|
security(("cookie_auth" = [])),
|
||||||
|
params(("id" = u64, Path, description = "File id")),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "File metadata", body = FileRecord),
|
||||||
|
(status = 404, description = "File not found"),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 500, description = "Internal server error")
|
||||||
|
)
|
||||||
|
)]
|
||||||
async fn get_file(
|
async fn get_file(
|
||||||
State(file_repository): State<FileRepository>,
|
State(file_repository): State<FileRepository>,
|
||||||
ctx: Ctx,
|
ctx: Ctx,
|
||||||
@@ -59,6 +102,20 @@ async fn get_file(
|
|||||||
Ok(Json(record))
|
Ok(Json(record))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Download a file
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/files/{id}/download",
|
||||||
|
tag = "files",
|
||||||
|
security(("cookie_auth" = [])),
|
||||||
|
params(("id" = u64, Path, description = "File id")),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "File bytes", content_type = "application/octet-stream"),
|
||||||
|
(status = 404, description = "File not found"),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 500, description = "Internal server error")
|
||||||
|
)
|
||||||
|
)]
|
||||||
async fn download_file(
|
async fn download_file(
|
||||||
State(file_repository): State<FileRepository>,
|
State(file_repository): State<FileRepository>,
|
||||||
ctx: Ctx,
|
ctx: Ctx,
|
||||||
@@ -70,6 +127,24 @@ async fn download_file(
|
|||||||
Ok(Body::from_stream(stream))
|
Ok(Body::from_stream(stream))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stream a byte range of a file (HTTP 206)
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/files/{id}/stream_part",
|
||||||
|
tag = "files",
|
||||||
|
security(("cookie_auth" = [])),
|
||||||
|
params(
|
||||||
|
("id" = u64, Path, description = "File id"),
|
||||||
|
("Range" = Option<String>, Header, description = "Byte range, e.g. bytes=0-1023")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 206, description = "Partial content"),
|
||||||
|
(status = 400, description = "Invalid range"),
|
||||||
|
(status = 404, description = "File not found"),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 500, description = "Internal server error")
|
||||||
|
)
|
||||||
|
)]
|
||||||
async fn stream_part(
|
async fn stream_part(
|
||||||
State(file_repository): State<FileRepository>,
|
State(file_repository): State<FileRepository>,
|
||||||
ctx: Ctx,
|
ctx: Ctx,
|
||||||
@@ -117,6 +192,20 @@ fn parse_range(headers: &HeaderMap, file_size: u64) -> Result<(u64, u64)> {
|
|||||||
Ok((start, end))
|
Ok((start, end))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete a file
|
||||||
|
#[utoipa::path(
|
||||||
|
delete,
|
||||||
|
path = "/api/files/{id}",
|
||||||
|
tag = "files",
|
||||||
|
security(("cookie_auth" = [])),
|
||||||
|
params(("id" = u64, Path, description = "File id")),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Deleted file metadata", body = FileRecord),
|
||||||
|
(status = 404, description = "File not found"),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 500, description = "Internal server error")
|
||||||
|
)
|
||||||
|
)]
|
||||||
async fn delete_file(
|
async fn delete_file(
|
||||||
State(file_repository): State<FileRepository>,
|
State(file_repository): State<FileRepository>,
|
||||||
ctx: Ctx,
|
ctx: Ctx,
|
||||||
@@ -128,6 +217,18 @@ async fn delete_file(
|
|||||||
Ok(Json(file))
|
Ok(Json(file))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// List the current user's files
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/files",
|
||||||
|
tag = "files",
|
||||||
|
security(("cookie_auth" = [])),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "List user's files", body = Vec<FileRecord>),
|
||||||
|
(status = 401, description = "Unauthorized"),
|
||||||
|
(status = 500, description = "Internal server error")
|
||||||
|
)
|
||||||
|
)]
|
||||||
async fn list_files(
|
async fn list_files(
|
||||||
State(file_repository): State<FileRepository>,
|
State(file_repository): State<FileRepository>,
|
||||||
ctx: Ctx,
|
ctx: Ctx,
|
||||||
@@ -138,7 +239,11 @@ async fn list_files(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use axum::{Router, http::header, middleware};
|
use axum::{
|
||||||
|
Router,
|
||||||
|
http::{StatusCode, header},
|
||||||
|
middleware,
|
||||||
|
};
|
||||||
use axum_test::{
|
use axum_test::{
|
||||||
TestServer,
|
TestServer,
|
||||||
multipart::{MultipartForm, Part},
|
multipart::{MultipartForm, Part},
|
||||||
@@ -284,7 +389,7 @@ mod tests {
|
|||||||
))
|
))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
res.assert_status_ok();
|
res.assert_status(StatusCode::CREATED);
|
||||||
let file = res.json::<serde_json::Value>();
|
let file = res.json::<serde_json::Value>();
|
||||||
assert_eq!(file["name"], "a.jpg");
|
assert_eq!(file["name"], "a.jpg");
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,21 @@
|
|||||||
use axum::{Router, routing::get};
|
use axum::{Router, routing::get};
|
||||||
|
use utoipa::OpenApi;
|
||||||
|
|
||||||
|
#[derive(OpenApi)]
|
||||||
|
#[openapi(paths(health))]
|
||||||
|
pub struct HealthApi;
|
||||||
|
|
||||||
pub fn routes_health() -> Router {
|
pub fn routes_health() -> Router {
|
||||||
Router::new().route("/health", get(health))
|
Router::new().route("/health", get(health))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Health check
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/health",
|
||||||
|
tag = "health",
|
||||||
|
responses((status = 200, description = "Service is healthy"))
|
||||||
|
)]
|
||||||
async fn health() -> &'static str {
|
async fn health() -> &'static str {
|
||||||
"up"
|
"up"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,13 @@ use argon2::{
|
|||||||
};
|
};
|
||||||
use axum::{Json, Router, extract::State, http::StatusCode, routing::post};
|
use axum::{Json, Router, extract::State, http::StatusCode, routing::post};
|
||||||
use rand::RngExt;
|
use rand::RngExt;
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use tower_cookies::{Cookie, Cookies};
|
use tower_cookies::{Cookie, Cookies};
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
use utoipa::{
|
||||||
|
Modify, OpenApi,
|
||||||
|
openapi::security::{ApiKey, ApiKeyValue, SecurityScheme},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::{LoftError, Result},
|
error::{LoftError, Result},
|
||||||
@@ -14,6 +18,32 @@ use crate::{
|
|||||||
web::AUTH_TOKEN,
|
web::AUTH_TOKEN,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(OpenApi)]
|
||||||
|
#[openapi(
|
||||||
|
info(
|
||||||
|
title = "loft API",
|
||||||
|
description = "Self-hosted media storage (Axum + SvelteKit)"
|
||||||
|
),
|
||||||
|
modifiers(&SecurityAddon),
|
||||||
|
paths(login, logout, register),
|
||||||
|
components(schemas(LoginPayload))
|
||||||
|
)]
|
||||||
|
pub struct AuthApi;
|
||||||
|
|
||||||
|
/// Registers the session-cookie auth scheme so protected endpoints show as secured.
|
||||||
|
struct SecurityAddon;
|
||||||
|
|
||||||
|
impl Modify for SecurityAddon {
|
||||||
|
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
|
||||||
|
if let Some(components) = openapi.components.as_mut() {
|
||||||
|
components.add_security_scheme(
|
||||||
|
"cookie_auth",
|
||||||
|
SecurityScheme::ApiKey(ApiKey::Cookie(ApiKeyValue::new(AUTH_TOKEN))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn routes_auth(user_repository: UserRepository) -> Router {
|
pub fn routes_auth(user_repository: UserRepository) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/login", post(login))
|
.route("/login", post(login))
|
||||||
@@ -22,6 +52,18 @@ pub fn routes_auth(user_repository: UserRepository) -> Router {
|
|||||||
.with_state(user_repository)
|
.with_state(user_repository)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Log in and start a session
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/auth/login",
|
||||||
|
tag = "auth",
|
||||||
|
request_body = LoginPayload,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Login successful"),
|
||||||
|
(status = 401, description = "Login failed"),
|
||||||
|
(status = 500, description = "Internal server error")
|
||||||
|
)
|
||||||
|
)]
|
||||||
async fn login(
|
async fn login(
|
||||||
State(user_repository): State<UserRepository>,
|
State(user_repository): State<UserRepository>,
|
||||||
cookies: Cookies,
|
cookies: Cookies,
|
||||||
@@ -50,6 +92,17 @@ async fn login(
|
|||||||
Ok(StatusCode::OK)
|
Ok(StatusCode::OK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Log out and clear the session
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/auth/logout",
|
||||||
|
tag = "auth",
|
||||||
|
// request_body = LoginPayload,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Logout successful"),
|
||||||
|
(status = 500, description = "Internal server error")
|
||||||
|
)
|
||||||
|
)]
|
||||||
async fn logout(
|
async fn logout(
|
||||||
State(user_repository): State<UserRepository>,
|
State(user_repository): State<UserRepository>,
|
||||||
cookies: Cookies,
|
cookies: Cookies,
|
||||||
@@ -64,6 +117,18 @@ async fn logout(
|
|||||||
Ok(StatusCode::OK)
|
Ok(StatusCode::OK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register a new account
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/auth/register",
|
||||||
|
tag = "auth",
|
||||||
|
// request_body = LoginPayload,
|
||||||
|
responses(
|
||||||
|
(status = 201, description = "Registration successful"),
|
||||||
|
(status = 401, description = "Registration failed"),
|
||||||
|
(status = 500, description = "Internal server error")
|
||||||
|
)
|
||||||
|
)]
|
||||||
async fn register(
|
async fn register(
|
||||||
State(user_repository): State<UserRepository>,
|
State(user_repository): State<UserRepository>,
|
||||||
Json(payload): Json<LoginPayload>,
|
Json(payload): Json<LoginPayload>,
|
||||||
@@ -76,7 +141,7 @@ async fn register(
|
|||||||
warn!(
|
warn!(
|
||||||
"Register fail, username {} already exists",
|
"Register fail, username {} already exists",
|
||||||
&payload.username
|
&payload.username
|
||||||
); // also fix "Login fail" typo
|
);
|
||||||
return Err(LoftError::RegisterFail);
|
return Err(LoftError::RegisterFail);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +173,7 @@ fn create_cookie() -> Cookie<'static> {
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
struct LoginPayload {
|
struct LoginPayload {
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
|
|||||||
Reference in New Issue
Block a user