diff options
| author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2025-12-09 13:07:14 +0100 |
|---|---|---|
| committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2025-12-09 13:07:14 +0100 |
| commit | c91dce4f77ae12453203f0a28b91efb6533cc095 (patch) | |
| tree | 4f50e755dff7f717d45309b08f9fe2c8c87f88bd /crates/rocie-server/src/api/set | |
| parent | chore(rocie-client): Regenerate (diff) | |
| download | server-c91dce4f77ae12453203f0a28b91efb6533cc095.zip | |
feat(rocie-server): Implement basic user handling and authentication
Diffstat (limited to '')
11 files changed, 301 insertions, 38 deletions
diff --git a/crates/rocie-server/src/api/set/barcode.rs b/crates/rocie-server/src/api/set/auth/barcode.rs index bb84bbf..1d97852 100644 --- a/crates/rocie-server/src/api/set/barcode.rs +++ b/crates/rocie-server/src/api/set/auth/barcode.rs @@ -1,3 +1,4 @@ +use actix_identity::Identity; use actix_web::{HttpResponse, Responder, Result, post, web}; use log::debug; @@ -22,20 +23,31 @@ use crate::{ description = "Barcode id was not found", ), ( + status = UNAUTHORIZED, + description = "You did not login before calling this endpoint", + ), + ( status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String, ) ), params( - ("barcode_id" = BarcodeId, description = "The numeric value of the barcode"), - ("times" = u16, description = "How often to buy the barcode"), + ( + "barcode_id" = BarcodeId, + description = "The numeric value of the barcode" + ), + ( + "times" = u16, + description = "How often to buy the barcode" + ), ) )] #[post("/barcode/{barcode_id}/buy/{times}")] pub(crate) async fn buy_barcode( app: web::Data<App>, path: web::Path<(BarcodeIdStub, u16)>, + _user: Identity, ) -> Result<impl Responder> { let (barcode_id, times) = path.into_inner(); @@ -69,13 +81,20 @@ pub(crate) async fn buy_barcode( description = "Barcode id was not found", ), ( + status = UNAUTHORIZED, + description = "You did not login before calling this endpoint", + ), + ( status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String, ) ), params( - ("id" = BarcodeId, description = "The numeric value of the barcode"), + ( + "id" = BarcodeId, + description = "The numeric value of the barcode" + ), ), request_body = UnitAmount, )] @@ -84,6 +103,7 @@ pub(crate) async fn consume_barcode( app: web::Data<App>, barcode_id: web::Path<BarcodeIdStub>, unit_amount: web::Json<UnitAmount>, + _user: Identity, ) -> Result<impl Responder> { let mut ops = Operations::new("consume barcode unit"); diff --git a/crates/rocie-server/src/api/set/auth/mod.rs b/crates/rocie-server/src/api/set/auth/mod.rs new file mode 100644 index 0000000..4e733a9 --- /dev/null +++ b/crates/rocie-server/src/api/set/auth/mod.rs @@ -0,0 +1,21 @@ +use actix_web::web; + +pub(crate) mod barcode; +pub(crate) mod product; +pub(crate) mod product_parent; +pub(crate) mod recipe; +pub(crate) mod unit; +pub(crate) mod unit_property; +pub(crate) mod user; + +pub(crate) fn register_paths(cfg: &mut web::ServiceConfig) { + cfg.service(product::register_product) + .service(product::associate_barcode) + .service(product_parent::register_product_parent) + .service(recipe::add_recipe) + .service(unit::register_unit) + .service(unit_property::register_unit_property) + .service(barcode::consume_barcode) + .service(barcode::buy_barcode) + .service(user::register_user); +} diff --git a/crates/rocie-server/src/api/set/product.rs b/crates/rocie-server/src/api/set/auth/product.rs index 74a92d2..b2a751f 100644 --- a/crates/rocie-server/src/api/set/product.rs +++ b/crates/rocie-server/src/api/set/auth/product.rs @@ -1,3 +1,4 @@ +use actix_identity::Identity; use actix_web::{HttpResponse, Responder, Result, post, web}; use serde::Deserialize; use utoipa::ToSchema; @@ -40,6 +41,10 @@ struct ProductStub { body = ProductId, ), ( + status = UNAUTHORIZED, + description = "You did not login before calling this endpoint", + ), + ( status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String, @@ -51,13 +56,15 @@ struct ProductStub { pub(crate) async fn register_product( app: web::Data<App>, product_stub: web::Json<ProductStub>, + _user: Identity, ) -> Result<impl Responder> { + let product_stub = product_stub.into_inner(); let mut ops = Operations::new("register product"); let product = Product::register( - product_stub.name.clone(), - product_stub.description.clone(), - product_stub.parent.into(), + product_stub.name, + product_stub.description, + product_stub.parent, product_stub.unit_property, &mut ops, ); @@ -79,7 +86,11 @@ pub(crate) async fn register_product( description = "Product id not found in database", ), ( - status = FORBIDDEN, + status = UNAUTHORIZED, + description = "You did not login before calling this endpoint", + ), + ( + status = BAD_REQUEST, description = "Unit used in request has not been registered yet", body = String, ), @@ -102,13 +113,14 @@ pub(crate) async fn associate_barcode( app: web::Data<App>, id: web::Path<ProductIdStub>, barcode: web::Json<Barcode>, + _user: Identity, ) -> Result<impl Responder> { let mut ops = Operations::new("associated barcode with product"); { let units = Unit::get_all(&app).await?; if !units.into_iter().any(|unit| unit.id == barcode.amount.unit) { - return Ok(HttpResponse::Forbidden() + return Ok(HttpResponse::BadRequest() .body("The used unit has not been registered; it cannot be used.\n")); } } diff --git a/crates/rocie-server/src/api/set/product_parent.rs b/crates/rocie-server/src/api/set/auth/product_parent.rs index f917207..416875b 100644 --- a/crates/rocie-server/src/api/set/product_parent.rs +++ b/crates/rocie-server/src/api/set/auth/product_parent.rs @@ -1,3 +1,4 @@ +use actix_identity::Identity; use actix_web::{HttpResponse, Responder, Result, post, web}; use serde::Deserialize; use utoipa::ToSchema; @@ -28,11 +29,15 @@ struct ProductParentStub { #[utoipa::path( responses( ( - status = 200, + status = OK, description = "Product parent successfully registered in database", body = ProductParentId, ), ( + status = UNAUTHORIZED, + description = "You did not login before calling this endpoint", + ), + ( status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String, @@ -44,13 +49,15 @@ struct ProductParentStub { pub(crate) async fn register_product_parent( app: web::Data<App>, product_stub: web::Json<ProductParentStub>, + _user: Identity, ) -> Result<impl Responder> { + let product_stub = product_stub.into_inner(); let mut ops = Operations::new("register product parent"); let product = ProductParent::register( - product_stub.name.clone(), - product_stub.description.clone(), - product_stub.parent.into(), + product_stub.name, + product_stub.description, + product_stub.parent, &mut ops, ); diff --git a/crates/rocie-server/src/api/set/recipe.rs b/crates/rocie-server/src/api/set/auth/recipe.rs index bb5be37..43a034e 100644 --- a/crates/rocie-server/src/api/set/recipe.rs +++ b/crates/rocie-server/src/api/set/auth/recipe.rs @@ -1,5 +1,6 @@ use std::path::PathBuf; +use actix_identity::Identity; use actix_web::{HttpResponse, Responder, error::Result, post, web}; use serde::Deserialize; use utoipa::ToSchema; @@ -26,11 +27,15 @@ struct RecipeStub { #[utoipa::path( responses( ( - status = 200, + status = OK, description = "Product parent successfully registered in database", body = RecipeId, ), ( + status = UNAUTHORIZED, + description = "You did not login before calling this endpoint", + ), + ( status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String, @@ -42,6 +47,7 @@ struct RecipeStub { pub(crate) async fn add_recipe( app: web::Data<App>, stub: web::Json<RecipeStub>, + _user: Identity, ) -> Result<impl Responder> { let stub = stub.into_inner(); let mut ops = Operations::new("add recipe parent"); diff --git a/crates/rocie-server/src/api/set/unit.rs b/crates/rocie-server/src/api/set/auth/unit.rs index 1671918..21d1e11 100644 --- a/crates/rocie-server/src/api/set/unit.rs +++ b/crates/rocie-server/src/api/set/auth/unit.rs @@ -1,3 +1,4 @@ +use actix_identity::Identity; use actix_web::{HttpResponse, Responder, Result, post, web}; use serde::Deserialize; use utoipa::ToSchema; @@ -26,11 +27,15 @@ struct UnitStub { #[utoipa::path( responses( ( - status = 200, + status = OK, description = "Unit successfully registered in database", body = UnitId, ), ( + status = UNAUTHORIZED, + description = "You did not login before calling this endpoint", + ), + ( status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String, @@ -42,14 +47,16 @@ struct UnitStub { pub(crate) async fn register_unit( app: web::Data<App>, unit: web::Json<UnitStub>, + _user: Identity, ) -> Result<impl Responder> { + let unit = unit.into_inner(); let mut ops = Operations::new("register unit"); let unit = Unit::register( - unit.full_name_singular.clone(), - unit.full_name_plural.clone(), - unit.short_name.clone(), - unit.description.clone(), + unit.full_name_singular, + unit.full_name_plural, + unit.short_name, + unit.description, unit.unit_property, &mut ops, ); diff --git a/crates/rocie-server/src/api/set/unit_property.rs b/crates/rocie-server/src/api/set/auth/unit_property.rs index ca2960f..2958e1f 100644 --- a/crates/rocie-server/src/api/set/unit_property.rs +++ b/crates/rocie-server/src/api/set/auth/unit_property.rs @@ -1,3 +1,4 @@ +use actix_identity::Identity; use actix_web::{HttpResponse, Responder, Result, post, web}; use serde::Deserialize; use utoipa::ToSchema; @@ -24,11 +25,15 @@ struct UnitPropertyStub { #[utoipa::path( responses( ( - status = 200, + status = OK, description = "Unit property successfully registered in database", body = UnitPropertyId, ), ( + status = UNAUTHORIZED, + description = "You did not login before calling this endpoint", + ), + ( status = INTERNAL_SERVER_ERROR, description = "Server encountered error", body = String, @@ -40,6 +45,7 @@ struct UnitPropertyStub { pub(crate) async fn register_unit_property( app: web::Data<App>, unit: web::Json<UnitPropertyStub>, + _user: Identity, ) -> Result<impl Responder> { let mut ops = Operations::new("register unit property"); diff --git a/crates/rocie-server/src/api/set/auth/user.rs b/crates/rocie-server/src/api/set/auth/user.rs new file mode 100644 index 0000000..1f262d5 --- /dev/null +++ b/crates/rocie-server/src/api/set/auth/user.rs @@ -0,0 +1,63 @@ +use actix_identity::Identity; +use actix_web::{HttpResponse, Responder, Result, post, web}; +use serde::Deserialize; +use utoipa::ToSchema; + +use crate::{ + app::App, + storage::sql::{ + insert::Operations, + user::{PasswordHash, User, UserId}, + }, +}; + +#[derive(Deserialize, ToSchema)] +pub(crate) struct UserStub { + /// The name of the new user. + pub(crate) name: String, + + /// The password of the new user. + pub(crate) password: String, + + /// An optional description of the new user. + #[schema(nullable = false)] + pub(crate) description: Option<String>, +} + +/// Register an new User +#[utoipa::path( + responses( + ( + status = OK, + description = "User successfully registered in database", + body = UserId, + ), + ( + status = UNAUTHORIZED, + description = "You did not login before calling this endpoint", + ), + ( + status = INTERNAL_SERVER_ERROR, + description = "Server encountered error", + body = String, + ) + ), + request_body = UserStub, +)] +#[post("/user/new")] +pub(crate) async fn register_user( + app: web::Data<App>, + new_user: web::Json<UserStub>, + _user: Identity, +) -> Result<impl Responder> { + let user = new_user.into_inner(); + + let mut ops = Operations::new("register user"); + + let password_hash = PasswordHash::from_password(&user.password); + let user = User::register(user.name, password_hash, user.description, &mut ops); + + ops.apply(&app).await?; + + Ok(HttpResponse::Ok().json(user.id)) +} diff --git a/crates/rocie-server/src/api/set/mod.rs b/crates/rocie-server/src/api/set/mod.rs index a6037b9..c6ee9ab 100644 --- a/crates/rocie-server/src/api/set/mod.rs +++ b/crates/rocie-server/src/api/set/mod.rs @@ -1,19 +1,2 @@ -use actix_web::web; - -pub(crate) mod barcode; -pub(crate) mod product; -pub(crate) mod product_parent; -pub(crate) mod recipe; -pub(crate) mod unit; -pub(crate) mod unit_property; - -pub(crate) fn register_paths(cfg: &mut web::ServiceConfig) { - cfg.service(product::register_product) - .service(product::associate_barcode) - .service(product_parent::register_product_parent) - .service(recipe::add_recipe) - .service(unit::register_unit) - .service(unit_property::register_unit_property) - .service(barcode::consume_barcode) - .service(barcode::buy_barcode); -} +pub(crate) mod auth; +pub(crate) mod no_auth; diff --git a/crates/rocie-server/src/api/set/no_auth/mod.rs b/crates/rocie-server/src/api/set/no_auth/mod.rs new file mode 100644 index 0000000..27783fc --- /dev/null +++ b/crates/rocie-server/src/api/set/no_auth/mod.rs @@ -0,0 +1,7 @@ +use actix_web::web; + +pub(crate) mod user; + +pub(crate) fn register_paths(cfg: &mut web::ServiceConfig) { + cfg.service(user::login).service(user::logout).service(user::provision); +} diff --git a/crates/rocie-server/src/api/set/no_auth/user.rs b/crates/rocie-server/src/api/set/no_auth/user.rs new file mode 100644 index 0000000..7acb482 --- /dev/null +++ b/crates/rocie-server/src/api/set/no_auth/user.rs @@ -0,0 +1,131 @@ +use actix_identity::Identity; +use actix_web::{HttpMessage, HttpRequest, HttpResponse, Responder, Result, post, web}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use crate::{ + api::set::auth::user::UserStub, + app::App, + storage::sql::{ + insert::Operations, + user::{PasswordHash, User, UserId}, + }, +}; + +#[derive(ToSchema, Deserialize, Serialize)] +struct LoginInfo { + /// The id of the user. + id: UserId, + + /// The password of the user. + password: String, +} + +/// Log in as a specific user +#[utoipa::path( + responses( + ( + status = OK, + description = "User logged in", + ), + ( + status = NOT_FOUND, + description = "User id not found" + ), + ( + status = FORBIDDEN, + description = "Password did not match" + ), + ( + status = INTERNAL_SERVER_ERROR, + description = "Server encountered error", + body = String + ) + ), + request_body = LoginInfo, +)] +#[post("/login")] +async fn login( + request: HttpRequest, + app: web::Data<App>, + info: web::Json<LoginInfo>, +) -> Result<impl Responder> { + let info = info.into_inner(); + + if let Some(user) = User::from_id(&app, info.id).await? { + if user.password_hash.verify(&info.password) { + Identity::login(&request.extensions(), info.id.to_string())?; + Ok(HttpResponse::Ok().finish()) + } else { + Ok(HttpResponse::Forbidden().finish()) + } + } else { + Ok(HttpResponse::NotFound().finish()) + } +} + +/// Log the current user out +#[utoipa::path( + responses( + ( + status = OK, + description = "User logged out", + ), + ( + status = INTERNAL_SERVER_ERROR, + description = "Server encountered error", + body = String + ) + ), +)] +#[post("/logout")] +async fn logout(user: Identity) -> impl Responder { + user.logout(); + HttpResponse::Ok() +} + +/// Provision this instance. +/// +/// This only works, if no users exist yet. +#[utoipa::path( + responses( + ( + status = OK, + description = "User created and logged in", + body = UserId, + ), + ( + status = FORBIDDEN, + description = "Instance already provisioned", + ), + ( + status = INTERNAL_SERVER_ERROR, + description = "Server encountered error", + body = String + ) + ), + request_body = UserStub, +)] +#[post("/provision")] +async fn provision( + request: HttpRequest, + app: web::Data<App>, + new_user: web::Json<UserStub>, +) -> Result<impl Responder> { + if User::get_all(&app).await?.is_empty() { + let user = new_user.into_inner(); + + let mut ops = Operations::new("register user (during provisioning)"); + + let password_hash = PasswordHash::from_password(&user.password); + let user = User::register(user.name, password_hash, user.description, &mut ops); + + ops.apply(&app).await?; + + Identity::login(&request.extensions(), user.id.to_string())?; + + Ok(HttpResponse::Ok().json(user.id)) + } else { + Ok(HttpResponse::Forbidden().finish()) + } +} |
