diff --git a/backend-actix/gamenight-api.yaml b/backend-actix/gamenight-api.yaml index 32fca5b..2e2c40e 100644 --- a/backend-actix/gamenight-api.yaml +++ b/backend-actix/gamenight-api.yaml @@ -203,6 +203,44 @@ paths: $ref: '#/components/requestBodies/RenameGameRequest' security: - JWT-Auth: [] + /own: + post: + responses: + '200': + description: "OK" + '401': + $ref: '#/components/responses/FailureResponse' + '422': + $ref: '#/components/responses/FailureResponse' + requestBody: + $ref: '#/components/requestBodies/OwnGameRequest' + security: + - JWT-Auth: [] + /disown: + post: + responses: + '200': + description: "OK" + '401': + $ref: '#/components/responses/FailureResponse' + '422': + $ref: '#/components/responses/FailureResponse' + requestBody: + $ref: '#/components/requestBodies/DisownGameRequest' + security: + - JWT-Auth: [] + /owned_games: + get: + responses: + '200': + $ref: "#/components/responses/GameIdsResponse" + '401': + $ref: '#/components/responses/FailureResponse' + '422': + $ref: '#/components/responses/FailureResponse' + security: + - JWT-Auth: [] + components: @@ -357,6 +395,11 @@ components: required: - id - name + GameIdsResponse: + type: array + items: + type: string + requestBodies: LoginRequest: content: @@ -413,6 +456,16 @@ components: application/json: schema: $ref: '#/components/schemas/RenameGameRequestBody' + OwnGameRequest: + content: + application/json: + schema: + $ref: '#/components/schemas/GameId' + DisownGameRequest: + content: + application/json: + schema: + $ref: '#/components/schemas/GameId' responses: TokenResponse: description: Example response @@ -466,6 +519,12 @@ components: application/json: schema: $ref: '#/components/schemas/Game' + GameIdsResponse: + description: A list of game ids. + content: + application/json: + schema: + $ref: '#/components/schemas/GameIdsResponse' securitySchemes: JWT-Auth: type: http diff --git a/backend-actix/src/main.rs b/backend-actix/src/main.rs index 98d6b96..b3cab40 100644 --- a/backend-actix/src/main.rs +++ b/backend-actix/src/main.rs @@ -52,6 +52,9 @@ async fn main() -> std::io::Result<()> { .service(get_game) .service(post_game) .service(post_rename_game) + .service(post_own_game) + .service(post_disown_game) + .service(get_owned_games) }) .bind(("::1", 8080))? .run() diff --git a/backend-actix/src/request/game.rs b/backend-actix/src/request/game.rs index 0f5fdd9..45c7416 100644 --- a/backend-actix/src/request/game.rs +++ b/backend-actix/src/request/game.rs @@ -1,5 +1,5 @@ use actix_web::{get, http::header::ContentType, post, web, HttpResponse, Responder}; -use gamenight_database::{game::{insert_game, load_game, rename_game}, DbPool, GetConnection}; +use gamenight_database::{game::{insert_game, load_game, rename_game}, owned_game::{disown_game, own_game, owned_games, OwnedGame}, DbPool, GetConnection}; use uuid::Uuid; use crate::{models::{add_game_request_body::AddGameRequestBody, game::Game, game_id::GameId, rename_game_request_body::RenameGameRequestBody}, request::{authorization::AuthUser, error::ApiError}}; @@ -60,4 +60,35 @@ pub async fn post_rename_game(pool: web::Data, _user: AuthUser, game_dat rename_game(&mut conn, Uuid::parse_str(&game_data.0.id)?, game_data.0.name)?; Ok(HttpResponse::Ok()) -} \ No newline at end of file +} + +#[post("/own")] +pub async fn post_own_game(pool: web::Data, user: AuthUser, game_id: web::Json) -> Result { + let mut conn = pool.get_conn(); + own_game(&mut conn, OwnedGame { user_id: user.0.id, game_id: Uuid::parse_str(&game_id.0.game_id)? })?; + + Ok(HttpResponse::Ok()) +} + +#[post("/disown")] +pub async fn post_disown_game(pool: web::Data, user: AuthUser, game_id: web::Json) -> Result { + let mut conn = pool.get_conn(); + disown_game(&mut conn, OwnedGame { user_id: user.0.id, game_id: Uuid::parse_str(&game_id.0.game_id)? })?; + + Ok(HttpResponse::Ok()) +} + +#[get("/owned_games")] +pub async fn get_owned_games(pool: web::Data, user: AuthUser) -> Result { + let mut conn = pool.get_conn(); + let game_ids = owned_games(&mut conn, user.0.id)?; + + let model : Vec = game_ids.iter().map(|x| { + x.to_string() + }).collect(); + + Ok(HttpResponse::Ok() + .content_type(ContentType::json()) + .body(serde_json::to_string(&model)?) + ) +} diff --git a/backend-actix/src/request/mod.rs b/backend-actix/src/request/mod.rs index 61a1d30..8d35323 100644 --- a/backend-actix/src/request/mod.rs +++ b/backend-actix/src/request/mod.rs @@ -22,3 +22,6 @@ pub use game::get_games; pub use game::get_game; pub use game::post_game; pub use game::post_rename_game; +pub use game::post_own_game; +pub use game::post_disown_game; +pub use game::get_owned_games; diff --git a/gamenight-api-client-rs/README.md b/gamenight-api-client-rs/README.md index 82b241c..b6b6097 100644 --- a/gamenight-api-client-rs/README.md +++ b/gamenight-api-client-rs/README.md @@ -27,6 +27,7 @@ All URIs are relative to *http://localhost:8080* Class | Method | HTTP request | Description ------------ | ------------- | ------------- | ------------- +*DefaultApi* | [**disown_post**](docs/DefaultApi.md#disown_post) | **POST** /disown | *DefaultApi* | [**game_get**](docs/DefaultApi.md#game_get) | **GET** /game | *DefaultApi* | [**game_post**](docs/DefaultApi.md#game_post) | **POST** /game | *DefaultApi* | [**games_get**](docs/DefaultApi.md#games_get) | **GET** /games | @@ -35,6 +36,8 @@ Class | Method | HTTP request | Description *DefaultApi* | [**get_token**](docs/DefaultApi.md#get_token) | **GET** /token | *DefaultApi* | [**join_post**](docs/DefaultApi.md#join_post) | **POST** /join | *DefaultApi* | [**leave_post**](docs/DefaultApi.md#leave_post) | **POST** /leave | +*DefaultApi* | [**own_post**](docs/DefaultApi.md#own_post) | **POST** /own | +*DefaultApi* | [**owned_games_get**](docs/DefaultApi.md#owned_games_get) | **GET** /owned_games | *DefaultApi* | [**participants_get**](docs/DefaultApi.md#participants_get) | **GET** /participants | Get all participants for a gamenight *DefaultApi* | [**post_gamenight**](docs/DefaultApi.md#post_gamenight) | **POST** /gamenight | *DefaultApi* | [**post_register**](docs/DefaultApi.md#post_register) | **POST** /user | diff --git a/gamenight-api-client-rs/docs/DefaultApi.md b/gamenight-api-client-rs/docs/DefaultApi.md index b9036dd..85ed454 100644 --- a/gamenight-api-client-rs/docs/DefaultApi.md +++ b/gamenight-api-client-rs/docs/DefaultApi.md @@ -4,6 +4,7 @@ All URIs are relative to *http://localhost:8080* Method | HTTP request | Description ------------- | ------------- | ------------- +[**disown_post**](DefaultApi.md#disown_post) | **POST** /disown | [**game_get**](DefaultApi.md#game_get) | **GET** /game | [**game_post**](DefaultApi.md#game_post) | **POST** /game | [**games_get**](DefaultApi.md#games_get) | **GET** /games | @@ -12,6 +13,8 @@ Method | HTTP request | Description [**get_token**](DefaultApi.md#get_token) | **GET** /token | [**join_post**](DefaultApi.md#join_post) | **POST** /join | [**leave_post**](DefaultApi.md#leave_post) | **POST** /leave | +[**own_post**](DefaultApi.md#own_post) | **POST** /own | +[**owned_games_get**](DefaultApi.md#owned_games_get) | **GET** /owned_games | [**participants_get**](DefaultApi.md#participants_get) | **GET** /participants | Get all participants for a gamenight [**post_gamenight**](DefaultApi.md#post_gamenight) | **POST** /gamenight | [**post_register**](DefaultApi.md#post_register) | **POST** /user | @@ -21,6 +24,34 @@ Method | HTTP request | Description +## disown_post + +> disown_post(game_id) + + +### Parameters + + +Name | Type | Description | Required | Notes +------------- | ------------- | ------------- | ------------- | ------------- +**game_id** | Option<[**GameId**](GameId.md)> | | | + +### Return type + + (empty response body) + +### Authorization + +[JWT-Auth](../README.md#JWT-Auth) + +### HTTP request headers + +- **Content-Type**: application/json +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + + ## game_get > models::Game game_get(game_id) @@ -243,6 +274,59 @@ Name | Type | Description | Required | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +## own_post + +> own_post(game_id) + + +### Parameters + + +Name | Type | Description | Required | Notes +------------- | ------------- | ------------- | ------------- | ------------- +**game_id** | Option<[**GameId**](GameId.md)> | | | + +### Return type + + (empty response body) + +### Authorization + +[JWT-Auth](../README.md#JWT-Auth) + +### HTTP request headers + +- **Content-Type**: application/json +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + + +## owned_games_get + +> Vec owned_games_get() + + +### Parameters + +This endpoint does not need any parameter. + +### Return type + +**Vec** + +### Authorization + +[JWT-Auth](../README.md#JWT-Auth) + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + + ## participants_get > models::Participants participants_get(gamenight_id) diff --git a/gamenight-api-client-rs/src/apis/default_api.rs b/gamenight-api-client-rs/src/apis/default_api.rs index 219085c..1e0af90 100644 --- a/gamenight-api-client-rs/src/apis/default_api.rs +++ b/gamenight-api-client-rs/src/apis/default_api.rs @@ -15,6 +15,15 @@ use crate::{apis::ResponseContent, models}; use super::{Error, configuration, ContentType}; +/// struct for typed errors of method [`disown_post`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum DisownPostError { + Status401(models::Failure), + Status422(models::Failure), + UnknownValue(serde_json::Value), +} + /// struct for typed errors of method [`game_get`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] @@ -86,6 +95,24 @@ pub enum LeavePostError { UnknownValue(serde_json::Value), } +/// struct for typed errors of method [`own_post`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum OwnPostError { + Status401(models::Failure), + Status422(models::Failure), + UnknownValue(serde_json::Value), +} + +/// struct for typed errors of method [`owned_games_get`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum OwnedGamesGetError { + Status401(models::Failure), + Status422(models::Failure), + UnknownValue(serde_json::Value), +} + /// struct for typed errors of method [`participants_get`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] @@ -140,6 +167,35 @@ pub enum UserGetError { } +pub async fn disown_post(configuration: &configuration::Configuration, game_id: Option) -> Result<(), Error> { + // add a prefix to parameters to efficiently prevent name collisions + let p_game_id = game_id; + + let uri_str = format!("{}/disown", configuration.base_path); + let mut req_builder = configuration.client.request(reqwest::Method::POST, &uri_str); + + if let Some(ref user_agent) = configuration.user_agent { + req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone()); + } + if let Some(ref token) = configuration.bearer_access_token { + req_builder = req_builder.bearer_auth(token.to_owned()); + }; + req_builder = req_builder.json(&p_game_id); + + let req = req_builder.build()?; + let resp = configuration.client.execute(req).await?; + + let status = resp.status(); + + if !status.is_client_error() && !status.is_server_error() { + Ok(()) + } else { + let content = resp.text().await?; + let entity: Option = serde_json::from_str(&content).ok(); + Err(Error::ResponseError(ResponseContent { status, content, entity })) + } +} + pub async fn game_get(configuration: &configuration::Configuration, game_id: Option) -> Result> { // add a prefix to parameters to efficiently prevent name collisions let p_game_id = game_id; @@ -420,6 +476,72 @@ pub async fn leave_post(configuration: &configuration::Configuration, gamenight_ } } +pub async fn own_post(configuration: &configuration::Configuration, game_id: Option) -> Result<(), Error> { + // add a prefix to parameters to efficiently prevent name collisions + let p_game_id = game_id; + + let uri_str = format!("{}/own", configuration.base_path); + let mut req_builder = configuration.client.request(reqwest::Method::POST, &uri_str); + + if let Some(ref user_agent) = configuration.user_agent { + req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone()); + } + if let Some(ref token) = configuration.bearer_access_token { + req_builder = req_builder.bearer_auth(token.to_owned()); + }; + req_builder = req_builder.json(&p_game_id); + + let req = req_builder.build()?; + let resp = configuration.client.execute(req).await?; + + let status = resp.status(); + + if !status.is_client_error() && !status.is_server_error() { + Ok(()) + } else { + let content = resp.text().await?; + let entity: Option = serde_json::from_str(&content).ok(); + Err(Error::ResponseError(ResponseContent { status, content, entity })) + } +} + +pub async fn owned_games_get(configuration: &configuration::Configuration, ) -> Result, Error> { + + let uri_str = format!("{}/owned_games", configuration.base_path); + let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str); + + if let Some(ref user_agent) = configuration.user_agent { + req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone()); + } + if let Some(ref token) = configuration.bearer_access_token { + req_builder = req_builder.bearer_auth(token.to_owned()); + }; + + let req = req_builder.build()?; + let resp = configuration.client.execute(req).await?; + + let status = resp.status(); + let content_type = resp + .headers() + .get("content-type") + .and_then(|v| v.to_str().ok()) + .unwrap_or("application/octet-stream"); + let content_type = super::ContentType::from(content_type); + + if !status.is_client_error() && !status.is_server_error() { + let content = resp.text().await?; + match content_type { + ContentType::Json => serde_json::from_str(&content).map_err(Error::from), + ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `Vec<String>`"))), + ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `Vec<String>`")))), + } + } else { + let content = resp.text().await?; + let entity: Option = serde_json::from_str(&content).ok(); + Err(Error::ResponseError(ResponseContent { status, content, entity })) + } +} + /// Retrieve the participants of a single gamenight by id. pub async fn participants_get(configuration: &configuration::Configuration, gamenight_id: Option) -> Result> { // add a prefix to parameters to efficiently prevent name collisions diff --git a/gamenight-cli/src/flows/disown.rs b/gamenight-cli/src/flows/disown.rs new file mode 100644 index 0000000..bb1d4be --- /dev/null +++ b/gamenight-cli/src/flows/disown.rs @@ -0,0 +1,36 @@ +use std::fmt::Display; + +use async_trait::async_trait; +use gamenight_api_client_rs::{apis::default_api::disown_post, models::GameId}; +use uuid::Uuid; + +use super::{Flow, FlowOutcome, FlowResult, GamenightState}; + +#[derive(Clone)] +pub struct Disown { + game_id: Uuid +} + +impl Disown { + pub fn new(game_id: Uuid) -> Self { + Self { + game_id + } + } +} + +#[async_trait] +impl<'a> Flow<'a> for Disown { + async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> { + let _ = disown_post(&state.api_configuration, Some(GameId{game_id: self.game_id.to_string()})).await?; + + clear_screen::clear(); + Ok((FlowOutcome::Successful, state)) + } +} + +impl Display for Disown { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Disown") + } +} \ No newline at end of file diff --git a/gamenight-cli/src/flows/mod.rs b/gamenight-cli/src/flows/mod.rs index a25d402..7315655 100644 --- a/gamenight-cli/src/flows/mod.rs +++ b/gamenight-cli/src/flows/mod.rs @@ -25,6 +25,8 @@ mod list_games; mod add_game; mod view_game; mod rename_game; +mod own; +mod disown; pub struct GamenightState { api_configuration: Configuration, diff --git a/gamenight-cli/src/flows/own.rs b/gamenight-cli/src/flows/own.rs new file mode 100644 index 0000000..055cd8b --- /dev/null +++ b/gamenight-cli/src/flows/own.rs @@ -0,0 +1,36 @@ +use std::fmt::Display; + +use async_trait::async_trait; +use gamenight_api_client_rs::{apis::default_api::own_post, models::GameId}; +use uuid::Uuid; + +use super::{Flow, FlowOutcome, FlowResult, GamenightState}; + +#[derive(Clone)] +pub struct Own { + game_id: Uuid +} + +impl Own { + pub fn new(game_id: Uuid) -> Self { + Self { + game_id + } + } +} + +#[async_trait] +impl<'a> Flow<'a> for Own { + async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> { + let _ = own_post(&state.api_configuration, Some(GameId{game_id: self.game_id.to_string()})).await?; + + clear_screen::clear(); + Ok((FlowOutcome::Successful, state)) + } +} + +impl Display for Own { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Own") + } +} \ No newline at end of file diff --git a/gamenight-cli/src/flows/view_game.rs b/gamenight-cli/src/flows/view_game.rs index 4990ae5..0038de2 100644 --- a/gamenight-cli/src/flows/view_game.rs +++ b/gamenight-cli/src/flows/view_game.rs @@ -1,8 +1,9 @@ -use gamenight_api_client_rs::{apis::default_api::game_get, models::GameId}; +use gamenight_api_client_rs::{apis::default_api::{game_get, owned_games_get}, models::GameId}; use inquire::Select; +use uuid::Uuid; -use crate::{domain::game::Game, flows::{exit::Exit, rename_game::RenameGame}}; +use crate::{domain::game::Game, flows::{disown::Disown, exit::Exit, own::Own, rename_game::RenameGame}}; use super::*; @@ -27,10 +28,24 @@ impl<'a> Flow<'a> for ViewGame { println!("{}", game); + let owned_games: Vec = owned_games_get(&state.api_configuration).await?.iter().map(|x| -> Result { Ok(Uuid::parse_str(&x)?) }).collect::, FlowError>>()?; + + println!("game_id {:?}, owned_games {:?}", game.id, owned_games); + + let own_or_disown: Box + Send> = + if owned_games.into_iter().find(|x| *x == game.id) != None { + Box::new(Disown::new(self.game.id)) + } + else { + Box::new(Own::new(self.game.id)) + }; + let options: Vec + Send>> = vec![ + own_or_disown, Box::new(RenameGame::new(game.clone())), Box::new(Exit::new()) ]; + let choice = Select::new("What do you want to do:", options) .prompt_skippable()?; diff --git a/gamenight-database/src/lib.rs b/gamenight-database/src/lib.rs index 488fc91..e7d5d43 100644 --- a/gamenight-database/src/lib.rs +++ b/gamenight-database/src/lib.rs @@ -4,6 +4,7 @@ pub mod schema; pub mod gamenight; pub mod gamenight_participants; pub mod game; +pub mod owned_game; use diesel::r2d2::ConnectionManager; use diesel::r2d2::ManageConnection; diff --git a/gamenight-database/src/owned_game.rs b/gamenight-database/src/owned_game.rs new file mode 100644 index 0000000..a54ce15 --- /dev/null +++ b/gamenight-database/src/owned_game.rs @@ -0,0 +1,28 @@ +use diesel::{dsl::{delete, insert_into}, prelude::{Insertable, Queryable}, BoolExpressionMethods, ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl}; +use serde::{Serialize, Deserialize}; +use uuid::Uuid; +use crate::{schema::owned_game, user::DatabaseError}; + +#[derive(Serialize, Deserialize, Debug, Insertable, Queryable)] +#[diesel(table_name = owned_game)] +pub struct OwnedGame { + pub user_id: Uuid, + pub game_id: Uuid +} + +pub fn own_game(conn: &mut PgConnection, owned_game: OwnedGame) -> Result{ + Ok(insert_into(owned_game::table).values(&owned_game).execute(conn)?) +} + +pub fn disown_game(conn: &mut PgConnection, owned_game: OwnedGame) -> Result { + Ok(delete(owned_game::table) + .filter(owned_game::user_id.eq(&owned_game.user_id) + .and(owned_game::game_id.eq(&owned_game.game_id)) + ).execute(conn)?) +} + +pub fn owned_games(conn: &mut PgConnection, uuid: Uuid) -> Result, DatabaseError> { + Ok(owned_game::table.select(owned_game::game_id) + .filter(owned_game::user_id.eq(uuid)) + .get_results(conn)?) +} \ No newline at end of file