add init utoipa crate openapi doc generation

This commit is contained in:
Mika 2025-06-15 11:28:28 +02:00
parent 6e887bfd8b
commit f9c724f53c
5 changed files with 256 additions and 13 deletions

163
Cargo.lock generated
View file

@ -409,6 +409,15 @@ version = "1.0.97"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
[[package]]
name = "arbitrary"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
dependencies = [
"derive_arbitrary",
]
[[package]] [[package]]
name = "arc-swap" name = "arc-swap"
version = "1.7.1" version = "1.7.1"
@ -631,6 +640,8 @@ dependencies = [
"temp-env", "temp-env",
"thiserror 2.0.12", "thiserror 2.0.12",
"tracing-actix-web", "tracing-actix-web",
"utoipa",
"utoipa-swagger-ui",
"uuid", "uuid",
"validator", "validator",
] ]
@ -1102,6 +1113,17 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "derive_arbitrary"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "0.99.19" version = "0.99.19"
@ -1826,6 +1848,7 @@ checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.15.2", "hashbrown 0.15.2",
"serde",
] ]
[[package]] [[package]]
@ -2695,6 +2718,40 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "rust-embed"
version = "8.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
"walkdir",
]
[[package]]
name = "rust-embed-impl"
version = "8.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c"
dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn 2.0.100",
"walkdir",
]
[[package]]
name = "rust-embed-utils"
version = "8.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594"
dependencies = [
"sha2",
"walkdir",
]
[[package]] [[package]]
name = "rust_decimal" name = "rust_decimal"
version = "1.36.0" version = "1.36.0"
@ -2804,6 +2861,15 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "scc" name = "scc"
version = "2.3.4" version = "2.3.4"
@ -3121,6 +3187,12 @@ dependencies = [
"rand_core 0.6.4", "rand_core 0.6.4",
] ]
[[package]]
name = "simd-adler32"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]] [[package]]
name = "simdutf8" name = "simdutf8"
version = "0.1.5" version = "0.1.5"
@ -3810,6 +3882,49 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "utoipa"
version = "5.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435c6f69ef38c9017b4b4eea965dfb91e71e53d869e896db40d1cf2441dd75c0"
dependencies = [
"indexmap",
"serde",
"serde_json",
"utoipa-gen",
]
[[package]]
name = "utoipa-gen"
version = "5.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a77d306bc75294fd52f3e99b13ece67c02c1a2789190a6f31d32f736624326f7"
dependencies = [
"proc-macro2",
"quote",
"regex",
"syn 2.0.100",
"uuid",
]
[[package]]
name = "utoipa-swagger-ui"
version = "9.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "161166ec520c50144922a625d8bc4925cc801b2dda958ab69878527c0e5c5d61"
dependencies = [
"actix-web",
"base64 0.22.1",
"mime_guess",
"regex",
"rust-embed",
"serde",
"serde_json",
"url",
"utoipa",
"zip",
]
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.16.0" version = "1.16.0"
@ -3874,6 +3989,16 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"
@ -3995,6 +4120,15 @@ dependencies = [
"wasite", "wasite",
] ]
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.52.0" version = "0.52.0"
@ -4296,6 +4430,35 @@ dependencies = [
"syn 2.0.100", "syn 2.0.100",
] ]
[[package]]
name = "zip"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
dependencies = [
"arbitrary",
"crc32fast",
"crossbeam-utils",
"displaydoc",
"flate2",
"indexmap",
"memchr",
"thiserror 2.0.12",
"zopfli",
]
[[package]]
name = "zopfli"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7"
dependencies = [
"bumpalo",
"crc32fast",
"log",
"simd-adler32",
]
[[package]] [[package]]
name = "zstd" name = "zstd"
version = "0.13.3" version = "0.13.3"

View file

@ -27,6 +27,10 @@ sea-orm = { version = "1.1", features = [
] } ] }
uuid = "1" uuid = "1"
utoipa = { version = "*", features = ["actix_extras", "chrono", "uuid"] }
# utoipa-actix-web = "*" # TODO: Test this for better actix-web integration
utoipa-swagger-ui = { version = "*", features = ["actix-web"] }
dotenvy = "0.15" dotenvy = "0.15"
[dev-dependencies] [dev-dependencies]

View file

@ -1,6 +1,6 @@
use actix_web::web::{self, ServiceConfig}; use actix_web::web::{self, ServiceConfig};
mod auth; pub mod auth; // TODO: Refactor to use re-exports instead of making module public
mod class; mod class;
mod group; mod group;
mod project; mod project;

View file

@ -4,22 +4,53 @@ use actix_web::{
web::{self, ServiceConfig}, web::{self, ServiceConfig},
}; };
use log::debug; use log::debug;
use serde::Deserialize; use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use crate::{Database, error::ApiError}; use crate::{Database, error::ApiError};
#[derive(Deserialize)] #[derive(Deserialize, ToSchema)]
struct LoginRequest { pub struct LoginRequest {
username: String, /// Username for authentication
password: String, pub username: String,
/// Password for authentication
pub password: String,
} }
#[derive(Serialize, ToSchema)]
pub struct LoginResponse {
/// Success message
pub message: String,
}
#[derive(Serialize, ToSchema)]
pub struct LogoutResponse {
/// Logout confirmation message
pub message: String,
}
// TODO: Implement generic ApiResponse<T> type to reduce boilerplate
pub fn setup(cfg: &mut ServiceConfig) { pub fn setup(cfg: &mut ServiceConfig) {
cfg.service(login).service(logout); cfg.service(login).service(logout);
} }
#[utoipa::path(
post,
path = "/api/v1/login",
tag = "auth",
summary = "User login",
description = "Authenticate a user with username and password",
request_body = LoginRequest,
responses(
(status = 200, description = "Login successful", body = LoginResponse),
(status = 400, description = "Invalid credentials"),
(status = 409, description = "User already logged in"),
(status = 500, description = "Internal server error")
)
)]
#[post("/login")] #[post("/login")]
async fn login( pub async fn login(
db: web::Data<Database>, db: web::Data<Database>,
login_request: web::Json<LoginRequest>, login_request: web::Json<LoginRequest>,
session: Session, session: Session,
@ -36,14 +67,29 @@ async fn login(
session.insert("user", user_id)?; session.insert("user", user_id)?;
Ok(HttpResponse::Ok()) Ok(HttpResponse::Ok().json(LoginResponse {
message: "Login successful".to_string(),
}))
} }
#[utoipa::path(
post,
path = "/api/v1/logout",
tag = "auth",
summary = "User logout",
description = "Log out the currently authenticated user and clear session",
responses(
(status = 200, description = "Logout successful", body = LogoutResponse),
(status = 500, description = "Internal server error")
)
)]
#[post("/logout")] #[post("/logout")]
async fn logout(session: Session, request: HttpRequest) -> Result<impl Responder, ApiError> { pub async fn logout(session: Session, request: HttpRequest) -> Result<impl Responder, ApiError> {
debug!("request cookies: {:?}", request.cookies()); debug!("request cookies: {:?}", request.cookies());
debug!("Session entries: {:?}", session.entries()); debug!("Session entries: {:?}", session.entries());
session.purge(); session.purge();
debug!("Session entries after purge: {:?}", session.entries()); debug!("Session entries after purge: {:?}", session.entries());
Ok(HttpResponse::Ok().body("Logged out successfully")) Ok(HttpResponse::Ok().json(LogoutResponse {
message: "Logged out successfully".to_string(),
}))
} }

View file

@ -1,9 +1,10 @@
use actix_files::NamedFile; use actix_files::NamedFile;
use actix_session::Session;
use actix_session::{SessionMiddleware, storage::RedisSessionStore}; use actix_session::{SessionMiddleware, storage::RedisSessionStore};
use actix_web::cookie::SameSite; use actix_web::cookie::SameSite;
use actix_web::{App, HttpResponse, HttpServer, cookie::Key, middleware::Logger, web}; use actix_web::{App, HttpServer, cookie::Key, middleware::Logger, web};
use log::debug; use log::debug;
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
mod controller; mod controller;
mod db; mod db;
@ -20,6 +21,31 @@ struct AppConfig {
ldap_auth: bool, ldap_auth: bool,
} }
#[derive(OpenApi)]
#[openapi(
info(
title = "PGG API",
description = "API for the PGG (Paket Verfolgungs Programm) application",
version = "1.0.0",
),
paths(
controller::auth::login,
controller::auth::logout,
),
components(schemas(
controller::auth::LoginRequest,
controller::auth::LoginResponse,
controller::auth::LogoutResponse,
)),
tags(
(name = "auth", description = "Authentication endpoints"),
(name = "users", description = "User management endpoints"),
(name = "projects", description = "Project management endpoints"),
(name = "groups", description = "Group management endpoints"),
)
)]
struct ApiDoc;
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
dotenvy::dotenv().ok(); dotenvy::dotenv().ok();
@ -59,7 +85,11 @@ async fn main() -> std::io::Result<()> {
.app_data(web::Data::new(app_config.clone())) .app_data(web::Data::new(app_config.clone()))
.wrap(Logger::default()) .wrap(Logger::default())
.wrap(session_middleware) .wrap(session_middleware)
.service(web::scope("/api/v1").configure(controller::register_controllers)); .service(web::scope("/api/v1").configure(controller::register_controllers))
.service(
SwaggerUi::new("/swagger-ui/{_:.*}")
.url("/api-docs/openapi.json", ApiDoc::openapi()),
);
#[cfg(feature = "serve")] #[cfg(feature = "serve")]
let app = { let app = {