diff --git a/Cargo.lock b/Cargo.lock index 11ee8fa..289ddfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -409,6 +409,15 @@ version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "arc-swap" version = "1.7.1" @@ -631,6 +640,8 @@ dependencies = [ "temp-env", "thiserror 2.0.12", "tracing-actix-web", + "utoipa", + "utoipa-swagger-ui", "uuid", "validator", ] @@ -1102,6 +1113,17 @@ dependencies = [ "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]] name = "derive_more" version = "0.99.19" @@ -1826,6 +1848,7 @@ checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" dependencies = [ "equivalent", "hashbrown 0.15.2", + "serde", ] [[package]] @@ -2695,6 +2718,40 @@ dependencies = [ "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]] name = "rust_decimal" version = "1.36.0" @@ -2804,6 +2861,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "scc" version = "2.3.4" @@ -3121,6 +3187,12 @@ dependencies = [ "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]] name = "simdutf8" version = "0.1.5" @@ -3810,6 +3882,49 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "uuid" version = "1.16.0" @@ -3874,6 +3989,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -3995,6 +4120,15 @@ dependencies = [ "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]] name = "windows-core" version = "0.52.0" @@ -4296,6 +4430,35 @@ dependencies = [ "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]] name = "zstd" version = "0.13.3" diff --git a/crates/backend/Cargo.toml b/crates/backend/Cargo.toml index 2b55dca..da0870e 100644 --- a/crates/backend/Cargo.toml +++ b/crates/backend/Cargo.toml @@ -27,6 +27,10 @@ sea-orm = { version = "1.1", features = [ ] } 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" [dev-dependencies] diff --git a/crates/backend/src/controller.rs b/crates/backend/src/controller.rs index edca1a5..6cf5f36 100644 --- a/crates/backend/src/controller.rs +++ b/crates/backend/src/controller.rs @@ -1,6 +1,6 @@ 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 group; mod project; diff --git a/crates/backend/src/controller/auth.rs b/crates/backend/src/controller/auth.rs index ebd00ea..407deaa 100644 --- a/crates/backend/src/controller/auth.rs +++ b/crates/backend/src/controller/auth.rs @@ -4,22 +4,53 @@ use actix_web::{ web::{self, ServiceConfig}, }; use log::debug; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; use crate::{Database, error::ApiError}; -#[derive(Deserialize)] -struct LoginRequest { - username: String, - password: String, +#[derive(Deserialize, ToSchema)] +pub struct LoginRequest { + /// Username for authentication + 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 type to reduce boilerplate + pub fn setup(cfg: &mut ServiceConfig) { 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")] -async fn login( +pub async fn login( db: web::Data, login_request: web::Json, session: Session, @@ -36,14 +67,29 @@ async fn login( 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")] -async fn logout(session: Session, request: HttpRequest) -> Result { +pub async fn logout(session: Session, request: HttpRequest) -> Result { debug!("request cookies: {:?}", request.cookies()); debug!("Session entries: {:?}", session.entries()); session.purge(); 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(), + })) } diff --git a/crates/backend/src/main.rs b/crates/backend/src/main.rs index 659192c..c37069e 100644 --- a/crates/backend/src/main.rs +++ b/crates/backend/src/main.rs @@ -1,9 +1,10 @@ use actix_files::NamedFile; -use actix_session::Session; use actix_session::{SessionMiddleware, storage::RedisSessionStore}; 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 utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; mod controller; mod db; @@ -20,6 +21,31 @@ struct AppConfig { 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] async fn main() -> std::io::Result<()> { dotenvy::dotenv().ok(); @@ -59,7 +85,11 @@ async fn main() -> std::io::Result<()> { .app_data(web::Data::new(app_config.clone())) .wrap(Logger::default()) .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")] let app = {