From f605463f32cc9a204b12120c53eb928adc9c621b Mon Sep 17 00:00:00 2001 From: TommyLike Date: Tue, 25 Apr 2023 09:47:07 +0800 Subject: [PATCH] Support delete user api keys --- src/application/user.rs | 22 ++++-- src/domain/token/entity.rs | 4 +- src/domain/token/repository.rs | 2 +- src/infra/database/model/token/repository.rs | 5 +- .../handler/control/model/token/dto.rs | 43 ++++-------- .../handler/control/user_handler.rs | 69 ++++++++++++++----- src/presentation/server/control_server.rs | 6 +- 7 files changed, 93 insertions(+), 58 deletions(-) diff --git a/src/application/user.rs b/src/application/user.rs index b873db8..b98d7f0 100644 --- a/src/application/user.rs +++ b/src/application/user.rs @@ -34,17 +34,18 @@ use openidconnect::{ }; use openidconnect::{JsonWebKeySet, ClientId, AuthUrl, UserInfoUrl, TokenUrl, RedirectUrl, ClientSecret, IssuerUrl}; use url::Url; -use crate::presentation::handler::control::model::token::dto::TokenDTO; +use crate::presentation::handler::control::model::token::dto::{CreateTokenDTO}; use crate::util::key::{generate_api_token}; #[async_trait] pub trait UserService: Send + Sync{ async fn get_token(&self, u: &UserIdentity) -> Result>; + async fn delete_token(&self, u: &UserIdentity, id: i32) -> Result<()>; async fn get_valid_token(&self, token: &str) -> Result; async fn save(&self, u: User) -> Result; async fn get_user_by_id(&self, id: i32) -> Result; async fn get_by_email(&self, email: &str) -> Result; - async fn generate_token(&self, u: &UserIdentity, token: TokenDTO) -> Result; + async fn generate_token(&self, u: &UserIdentity, token: CreateTokenDTO) -> Result; async fn get_login_url(&self) -> Result; async fn validate_user(&self, code: &str) -> Result; } @@ -158,6 +159,14 @@ where self.token_repository.get_token_by_user_id(user.id).await } + async fn delete_token(&self, u: &UserIdentity, id: i32) -> Result<()> { + let token = self.token_repository.get_token_by_id(id).await?; + if token.user_id != u.id { + return Err(Error::UnauthorizedError) + } + self.token_repository.delete_by_user_and_id(id, u.id).await + } + async fn get_valid_token(&self, token: &str) -> Result { let token = self.token_repository.get_token_by_value(token).await?; if token.expire_at.gt(&Utc::now()) { @@ -178,12 +187,13 @@ where self.user_repository.get_by_email(email).await } - async fn generate_token(&self, u: &UserIdentity, token: TokenDTO) -> Result { + async fn generate_token(&self, u: &UserIdentity, token: CreateTokenDTO) -> Result { let real_token = generate_api_token(); - let created = Token::new(token.id, u.id, token.description, real_token)?; - self.token_repository.create(created.clone()).await?; + let created = Token::new(u.id, token.description, real_token.clone())?; + let mut new = self.token_repository.create(created).await?; //return token with un-hashed value - Ok(created) + new.token = real_token; + Ok(new) } diff --git a/src/domain/token/entity.rs b/src/domain/token/entity.rs index 1ff01f2..1c17f56 100644 --- a/src/domain/token/entity.rs +++ b/src/domain/token/entity.rs @@ -42,10 +42,10 @@ impl Display for Token { } impl Token { - pub fn new(id: i32, user_id: i32, description: String, token: String) -> Result { + pub fn new(user_id: i32, description: String, token: String) -> Result { let now = Utc::now(); Ok(Token { - id, + id: 0, user_id, description, token, diff --git a/src/domain/token/repository.rs b/src/domain/token/repository.rs index 8a6ff30..b17e527 100644 --- a/src/domain/token/repository.rs +++ b/src/domain/token/repository.rs @@ -23,6 +23,6 @@ pub trait Repository: Send + Sync { async fn create(&self, user: Token) -> Result; async fn get_token_by_id(&self, id: i32) -> Result; async fn get_token_by_value(&self, token: &str) -> Result; - async fn delete_by_id(&self, id: i32) -> Result<()>; + async fn delete_by_user_and_id(&self, id: i32, user_id: i32) -> Result<()>; async fn get_token_by_user_id(&self, id: i32) -> Result>; } diff --git a/src/infra/database/model/token/repository.rs b/src/infra/database/model/token/repository.rs index 4d276a3..5d87c86 100644 --- a/src/infra/database/model/token/repository.rs +++ b/src/infra/database/model/token/repository.rs @@ -70,9 +70,10 @@ impl Repository for TokenRepository { Ok(Token::from(selected)) } - async fn delete_by_id(&self, id: i32) -> Result<()> { - let _: Option = sqlx::query_as("DELETE FROM token where id = ?") + async fn delete_by_user_and_id(&self, id: i32, user_id: i32) -> Result<()> { + let _: Option = sqlx::query_as("DELETE FROM token where id = ? AND user_id = ?") .bind(id) + .bind(user_id) .fetch_optional(&self.db_pool) .await?; Ok(()) diff --git a/src/presentation/handler/control/model/token/dto.rs b/src/presentation/handler/control/model/token/dto.rs index ad79876..45723ad 100644 --- a/src/presentation/handler/control/model/token/dto.rs +++ b/src/presentation/handler/control/model/token/dto.rs @@ -4,12 +4,17 @@ use serde::{Deserialize, Serialize}; use std::convert::From; -use chrono::{DateTime, Utc}; + use crate::domain::token::entity::Token; use utoipa::{ToSchema}; +#[derive(Debug, Deserialize, ToSchema)] +pub struct CreateTokenDTO { + pub description: String, +} + -#[derive(Debug, Deserialize, Serialize, ToSchema)] +#[derive(Debug, Serialize, ToSchema)] pub struct TokenDTO { #[serde(skip_deserializing)] pub id: i32, @@ -19,35 +24,15 @@ pub struct TokenDTO { pub token: String, pub description: String, #[serde(skip_deserializing)] - pub create_at: DateTime, + pub create_at: String, #[serde(skip_deserializing)] - pub expire_at: DateTime + pub expire_at: String } -impl TokenDTO { - pub fn new(description: String) -> TokenDTO { - TokenDTO { - id: 0, - user_id: 0, - //disable parse hash to dto - token: "".to_string(), +impl CreateTokenDTO { + pub fn new(description: String) -> CreateTokenDTO { + CreateTokenDTO { description, - expire_at: Default::default(), - create_at: Default::default(), - } - } -} - - -impl From for Token { - fn from(token: TokenDTO) -> Self { - Token { - id: 0, - user_id: 0, - description: token.description.clone(), - token: Default::default(), - create_at: Default::default(), - expire_at: Default::default(), } } } @@ -59,8 +44,8 @@ impl From for TokenDTO { user_id: token.user_id, token: token.token.clone(), description: token.description.clone(), - expire_at: token.expire_at, - create_at: token.create_at, + expire_at: token.expire_at.to_string(), + create_at: token.create_at.to_string(), } } } \ No newline at end of file diff --git a/src/presentation/handler/control/user_handler.rs b/src/presentation/handler/control/user_handler.rs index 3c77dc8..5a68d0a 100644 --- a/src/presentation/handler/control/user_handler.rs +++ b/src/presentation/handler/control/user_handler.rs @@ -21,7 +21,7 @@ use super::model::user::dto::UserIdentity; use actix_identity::Identity; use crate::application::user::UserService; -use crate::presentation::handler::control::model::token::dto::TokenDTO; +use crate::presentation::handler::control::model::token::dto::{CreateTokenDTO, TokenDTO}; #[derive(Deserialize)] struct Code { @@ -33,11 +33,11 @@ struct Code { /// ## Example /// Call the api endpoint with following curl. /// ```text -/// curl https://domain:port/api/v1/user/login +/// curl https://domain:port/api/v1/users/login /// ``` #[utoipa::path( get, - path = "/api/v1/user/login", + path = "/api/v1/users/login", responses( (status = 302, description = "Redirect to login url"), (status = 500, description = "Server internal error", body = ErrorMessage) @@ -52,11 +52,14 @@ async fn login(user_service: web::Data) -> Result Result { /// ## Example /// Call the api endpoint with following curl. /// ```text -/// curl -X POST https://domain:port/api/v1/user/logout +/// curl -X POST https://domain:port/api/v1/users/logout /// ``` #[utoipa::path( post, - path = "/api/v1/user/logout", + path = "/api/v1/users/logout", responses( (status = 204, description = "logout successfully"), (status = 500, description = "Server internal error", body = ErrorMessage) @@ -91,11 +94,11 @@ async fn logout(id: Identity) -> Result { /// ## Example /// Call the api endpoint with following curl. /// ```text -/// curl -X GET https://domain:port/api/v1/user/callback +/// curl -X GET https://domain:port/api/v1/users/callback /// ``` #[utoipa::path( get, - path = "/api/v1/user/callback", + path = "/api/v1/users/callback", responses( (status = 302, description = "logout succeed, redirect to index"), (status = 500, description = "Server internal error", body = ErrorMessage) @@ -118,11 +121,12 @@ async fn callback(req: HttpRequest, user_service: web::Data, co /// ## Example /// Call the api endpoint with following curl. /// ```text -/// curl -X POST https://domain:port/api/v1/user/api_keys +/// curl -X POST https://domain:port/api/v1/users/api_keys /// ``` #[utoipa::path( post, - path = "/api/v1/user/api_keys", + path = "/api/v1/users/api_keys", + request_body = CreateTokenDTO, security( ("Authorization" = []) ), @@ -131,22 +135,51 @@ async fn callback(req: HttpRequest, user_service: web::Data, co (status = 500, description = "Server internal error", body = ErrorMessage) ) )] -async fn new_token(user: UserIdentity, user_service: web::Data, token: web::Json) -> Result { +async fn new_token(user: UserIdentity, user_service: web::Data, token: web::Json) -> Result { let token = user_service.into_inner().generate_token(&user, token.0).await?; Ok(HttpResponse::Created().json(TokenDTO::from(token))) } +/// Delete specified user token +/// +/// ## Example +/// Call the api endpoint with following curl. +/// ```text +/// curl -X DELETE https://domain:port/api/v1/users/api_keys/{id} +/// ``` +#[utoipa::path( + delete, + path = "/api/v1/users/api_keys/{id}", + params( + ("id" = i32, Path, description = "Token id"), + ), + security( + ("Authorization" = []) + ), + responses( + (status = 200, description = "Token successfully deleted"), + (status = 400, description = "Bad request", body = ErrorMessage), + (status = 401, description = "Unauthorized", body = ErrorMessage), + (status = 404, description = "Key not found", body = ErrorMessage), + (status = 500, description = "Server internal error", body = ErrorMessage) + ) +)] +async fn delete_token(user: UserIdentity, user_service: web::Data, id: web::Path) -> Result { + user_service.into_inner().delete_token(&user, id.parse::()?).await?; + Ok(HttpResponse::Ok()) +} + /// List all tokens for current user /// -/// **NOTE**: only the token hash will be responsed. +/// **NOTE**: only the token hash will be responded. /// ## Example /// Call the api endpoint with following curl. /// ```text -/// curl -X GET https://domain:port/api/v1/user/api_keys +/// curl -X GET https://domain:port/api/v1/users/api_keys /// ``` #[utoipa::path( get, - path = "/api/v1/user/api_keys", + path = "/api/v1/users/api_keys", security( ("Authorization" = []) ), @@ -170,5 +203,9 @@ pub fn get_scope() -> Scope { .service(web::resource("/login").route(web::get().to(login))) .service(web::resource("/logout").route(web::post().to(logout))) .service(web::resource("/callback").route(web::get().to(callback))) - .service(web::resource("/api_keys").route(web::post().to(new_token)).route(web::get().to(list_token))) + .service(web::resource("/api_keys") + .route(web::post().to(new_token)) + .route(web::get().to(list_token))) + .service( web::resource("/api_keys/{id}") + .route(web::delete().to(delete_token))) } \ No newline at end of file diff --git a/src/presentation/server/control_server.rs b/src/presentation/server/control_server.rs index ecf5943..44a47a8 100644 --- a/src/presentation/server/control_server.rs +++ b/src/presentation/server/control_server.rs @@ -44,7 +44,7 @@ use crate::application::user::{DBUserService, UserService}; use crate::domain::datakey::entity::DataKey; use crate::domain::token::entity::Token; use crate::domain::user::entity::User; -use crate::presentation::handler::control::model::token::dto::TokenDTO; +use crate::presentation::handler::control::model::token::dto::{CreateTokenDTO}; use crate::presentation::handler::control::model::user::dto::UserIdentity; pub struct ControlServer { @@ -84,6 +84,7 @@ impl Modify for SecurityAddon { crate::presentation::handler::control::user_handler::logout, crate::presentation::handler::control::user_handler::new_token, crate::presentation::handler::control::user_handler::list_token, + crate::presentation::handler::control::user_handler::delete_token, ), components( schemas(crate::presentation::handler::control::model::datakey::dto::DataKeyDTO, @@ -91,6 +92,7 @@ impl Modify for SecurityAddon { crate::presentation::handler::control::model::datakey::dto::ImportDataKeyDTO, crate::presentation::handler::control::model::datakey::dto::ExportKey, crate::presentation::handler::control::model::token::dto::TokenDTO, + crate::presentation::handler::control::model::token::dto::CreateTokenDTO, crate::presentation::handler::control::model::user::dto::UserIdentity, crate::util::error::ErrorMessage) ), @@ -229,7 +231,7 @@ impl ControlServer { let user = self.user_service.save(user).await?; self.user_service.generate_token( &UserIdentity::from(user.clone()), - TokenDTO::new("default admin token".to_owned())).await + CreateTokenDTO::new("default admin token".to_owned())).await } //used for control admin cmd -- Gitee