diff --git a/backend-actix/gamenight-api.yaml b/backend-actix/gamenight-api.yaml index 2ae3480..2c1dcac 100644 --- a/backend-actix/gamenight-api.yaml +++ b/backend-actix/gamenight-api.yaml @@ -26,6 +26,19 @@ paths: $ref: '#/components/requestBodies/LoginRequest' description: Submit your credentials to get a JWT-token to use with the rest of the api. parameters: [] + post: + summary: '' + operationId: post-token + responses: + '200': + $ref: '#/components/responses/TokenResponse' + '401': + $ref: '#/components/responses/FailureResponse' + description: Refresh your JWT-token without logging in again. + parameters: [] + security: + - JWT-Auth: [] + /user: post: summary: '' diff --git a/backend-actix/src/main.rs b/backend-actix/src/main.rs index f10b40a..8bd35a7 100644 --- a/backend-actix/src/main.rs +++ b/backend-actix/src/main.rs @@ -38,6 +38,7 @@ async fn main() -> std::io::Result<()> { .wrap(TracingLogger::default()) .app_data(web::Data::new(pool.clone())) .service(login) + .service(refresh) .service(register) .service(gamenights) .service(gamenight_post) @@ -45,6 +46,7 @@ async fn main() -> std::io::Result<()> { .service(get_user) .service(get_user_unauthenticated) .service(post_join_gamenight) + .service(post_leave_gamenight) .service(get_get_participants) }) .bind(("::1", 8080))? diff --git a/backend-actix/src/request/authorization.rs b/backend-actix/src/request/authorization.rs index 9675921..26d8d79 100644 --- a/backend-actix/src/request/authorization.rs +++ b/backend-actix/src/request/authorization.rs @@ -6,7 +6,7 @@ use jsonwebtoken::{encode, Header, EncodingKey, decode, DecodingKey, Validation} use serde::{Serialize, Deserialize}; use uuid::Uuid; -use gamenight_database::{user::{get_user, Role, User}, DbPool}; +use gamenight_database::{user::{get_user, User}, DbPool}; use super::error::ApiError; @@ -16,21 +16,18 @@ pub struct Claims { uid: Uuid } -pub struct AuthUser { - pub id: Uuid, - pub username: String, - pub email: String, - pub role: Role, -} +pub struct AuthUser(pub User); + +// pub struct AuthUser { +// pub id: Uuid, +// pub username: String, +// pub email: String, +// pub role: Role, +// } impl From for AuthUser { fn from(value: User) -> Self { - Self{ - id: value.id, - username: value.username, - email: value.email, - role: value.role, - } + Self(value) } } diff --git a/backend-actix/src/request/gamenight_handlers.rs b/backend-actix/src/request/gamenight_handlers.rs index 04adf83..4688726 100644 --- a/backend-actix/src/request/gamenight_handlers.rs +++ b/backend-actix/src/request/gamenight_handlers.rs @@ -14,7 +14,7 @@ impl AddGamenightRequestBody { datetime: DateTime::parse_from_rfc3339(&self.clone().datetime.unwrap())?.with_timezone(&chrono::Utc), id: Uuid::new_v4(), name: self.clone().name.unwrap().clone(), - owner_id: user.id + owner_id: user.0.id }) } } diff --git a/backend-actix/src/request/join_gamenight.rs b/backend-actix/src/request/join_gamenight.rs index 40d778c..96dd1c4 100644 --- a/backend-actix/src/request/join_gamenight.rs +++ b/backend-actix/src/request/join_gamenight.rs @@ -10,7 +10,7 @@ pub async fn post_join_gamenight(pool: web::Data, user: AuthUser, gameni let mut conn = pool.get_conn(); Ok(insert_gamenight_participant(&mut conn, GamenightParticipant { gamenight_id: Uuid::parse_str(&gamenight_id.gamenight_id)?, - user_id: user.id + user_id: user.0.id })?) }).await??; @@ -21,10 +21,16 @@ pub async fn post_join_gamenight(pool: web::Data, user: AuthUser, gameni pub async fn post_leave_gamenight(pool: web::Data, user: AuthUser, gamenight_id: web::Json) -> Result { web::block(move || -> Result { let mut conn = pool.get_conn(); - Ok(delete_gamenight_participant(&mut conn, GamenightParticipant { + let participant = GamenightParticipant { gamenight_id: Uuid::parse_str(&gamenight_id.gamenight_id)?, - user_id: user.id - })?) + user_id: user.0.id + }; + println!("{:?}", participant); + let x = delete_gamenight_participant(&mut conn, participant)?; + + println!("Amount of deleted rows: {:?}", x); + + Ok(x) }).await??; Ok(HttpResponse::Ok()) diff --git a/backend-actix/src/request/mod.rs b/backend-actix/src/request/mod.rs index 73663fb..6a16ca0 100644 --- a/backend-actix/src/request/mod.rs +++ b/backend-actix/src/request/mod.rs @@ -7,6 +7,7 @@ mod join_gamenight; mod participant_handlers; pub use user_handlers::login; +pub use user_handlers::refresh; pub use user_handlers::register; pub use gamenight_handlers::gamenights; pub use gamenight_handlers::gamenight_post; @@ -14,4 +15,5 @@ pub use gamenight_handlers::gamenight_get; pub use user_handlers::get_user; pub use user_handlers::get_user_unauthenticated; pub use join_gamenight::post_join_gamenight; +pub use join_gamenight::post_leave_gamenight; pub use participant_handlers::get_get_participants; diff --git a/backend-actix/src/request/user_handlers.rs b/backend-actix/src/request/user_handlers.rs index a91b33e..dd47516 100644 --- a/backend-actix/src/request/user_handlers.rs +++ b/backend-actix/src/request/user_handlers.rs @@ -105,14 +105,22 @@ pub async fn login(pool: web::Data, login_data: web::Json) -> Res let response = Token{ jwt_token: Some(token) }; Ok(HttpResponse::Ok() .content_type(ContentType::json()) - .body(serde_json::to_string(&response)?) - ) + .body(serde_json::to_string(&response)?)) } else { Err(ApiError{status: 401, message: "User doesn't exist or password doesn't match".to_string()}) } } +#[post("/token")] +pub async fn refresh(user: AuthUser) -> Result { + let new_token = get_token(&user.0)?; + let response = Token{ jwt_token: Some(new_token) }; + Ok(HttpResponse::Ok() + .content_type(ContentType::json()) + .body(serde_json::to_string(&response)?)) +} + #[post("/user")] pub async fn register(pool: web::Data, register_data: web::Json) -> Result { web::block(move || -> Result<(), ApiError> { diff --git a/gamenight-api-client-rs/README.md b/gamenight-api-client-rs/README.md index 29faf9d..466608d 100644 --- a/gamenight-api-client-rs/README.md +++ b/gamenight-api-client-rs/README.md @@ -35,6 +35,7 @@ Class | Method | HTTP request | Description *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 | +*DefaultApi* | [**post_token**](docs/DefaultApi.md#post_token) | **POST** /token | *DefaultApi* | [**user_get**](docs/DefaultApi.md#user_get) | **GET** /user | diff --git a/gamenight-api-client-rs/docs/DefaultApi.md b/gamenight-api-client-rs/docs/DefaultApi.md index 5b73ddd..5dbcd02 100644 --- a/gamenight-api-client-rs/docs/DefaultApi.md +++ b/gamenight-api-client-rs/docs/DefaultApi.md @@ -12,6 +12,7 @@ Method | HTTP request | Description [**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 | +[**post_token**](DefaultApi.md#post_token) | **POST** /token | [**user_get**](DefaultApi.md#user_get) | **GET** /user | @@ -247,6 +248,33 @@ 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) +## post_token + +> models::Token post_token() + + +Refresh your JWT-token without logging in again. + +### Parameters + +This endpoint does not need any parameter. + +### Return type + +[**models::Token**](Token.md) + +### 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) + + ## user_get > models::User user_get(user_id) diff --git a/gamenight-api-client-rs/src/apis/default_api.rs b/gamenight-api-client-rs/src/apis/default_api.rs index f3f2ef1..f59fcef 100644 --- a/gamenight-api-client-rs/src/apis/default_api.rs +++ b/gamenight-api-client-rs/src/apis/default_api.rs @@ -85,6 +85,14 @@ pub enum PostRegisterError { UnknownValue(serde_json::Value), } +/// struct for typed errors of method [`post_token`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum PostTokenError { + Status401(models::Failure), + UnknownValue(serde_json::Value), +} + /// struct for typed errors of method [`user_get`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] @@ -371,6 +379,44 @@ pub async fn post_register(configuration: &configuration::Configuration, registr } } +/// Refresh your JWT-token without logging in again. +pub async fn post_token(configuration: &configuration::Configuration, ) -> Result> { + + let uri_str = format!("{}/token", 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()); + }; + + 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 `models::Token`"))), + ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::Token`")))), + } + } else { + let content = resp.text().await?; + let entity: Option = serde_json::from_str(&content).ok(); + Err(Error::ResponseError(ResponseContent { status, content, entity })) + } +} + /// Get a user from primary id pub async fn user_get(configuration: &configuration::Configuration, user_id: Option) -> Result> { // add a prefix to parameters to efficiently prevent name collisions diff --git a/gamenight-cli/Cargo.lock b/gamenight-cli/Cargo.lock index 5746f7f..8ba7a00 100644 --- a/gamenight-cli/Cargo.lock +++ b/gamenight-cli/Cargo.lock @@ -129,6 +129,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clear_screen" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0114f83ad196a822188f44e3def7916956705df289e6634d326ad86b3f0e9c" +dependencies = [ + "cc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -269,6 +278,7 @@ version = "0.1.0" dependencies = [ "async-trait", "chrono", + "clear_screen", "dyn-clone", "gamenight-api-client-rs", "inquire", diff --git a/gamenight-cli/Cargo.toml b/gamenight-cli/Cargo.toml index d671c71..599352e 100644 --- a/gamenight-cli/Cargo.toml +++ b/gamenight-cli/Cargo.toml @@ -14,3 +14,4 @@ uuid = { version = "1.3.0", features = ["serde", "v4"] } jsonwebtoken = "9.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +clear_screen = "0.1" diff --git a/gamenight-cli/src/domain/config.rs b/gamenight-cli/src/domain/config.rs new file mode 100644 index 0000000..0e2fdb5 --- /dev/null +++ b/gamenight-cli/src/domain/config.rs @@ -0,0 +1,85 @@ +use std::{env, fs::{self}, path::{Path, PathBuf}}; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug)] +pub struct ConfigError(pub String); + +impl From for ConfigError { + fn from(value: serde_json::Error) -> Self { + Self(value.to_string()) + } +} + +impl From for ConfigError { + fn from(value: std::io::Error) -> Self { + Self(value.to_string()) + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Instance { + pub name: String, + pub url: String, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub token: Option +} + +impl Instance { + pub fn new(name: String, url: String) -> Self { + Self { + name, + url, + token: None + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Config { + pub instances: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub last_instance: Option, +} + +impl Config { + fn config_path() -> PathBuf { + let mut prefix = Path::new(&env::var("HOME").expect("HOME environment variable was not set")).join(".config"); + if let Ok(config_home) = env::var("XDG_CONFIG_HOME") { + prefix = config_home.into(); + } + + prefix.join("gamenight-cli").join("config.json") + } + + pub fn new() -> Config { + Config { + instances: vec![], + last_instance: None + } + } + + pub fn load() -> Result { + let config_path = Self::config_path(); + if !fs::exists(&config_path)? { + let config_error = ConfigError(format!("Cannot create parent directory for config file: {}", &config_path.display()).to_string()); + let _ = fs::create_dir_all(&config_path.parent().ok_or(config_error)?)?; + + let config = Config::new(); + fs::write(&config_path, serde_json::to_string_pretty(&config)?.as_bytes())?; + Ok(config) + } + else { + let config_string = fs::read_to_string(Self::config_path())?; + Ok(serde_json::from_str(&config_string)?) + } + } + + pub fn save(gamenight_configuration: &Config) -> Result<(), ConfigError> { + let config_path = Self::config_path(); + fs::write(&config_path, serde_json::to_string_pretty(gamenight_configuration)?.as_bytes())?; + Ok(()) + } +} diff --git a/gamenight-cli/src/domain/gamenight.rs b/gamenight-cli/src/domain/gamenight.rs index 53dd8c4..29b0030 100644 --- a/gamenight-cli/src/domain/gamenight.rs +++ b/gamenight-cli/src/domain/gamenight.rs @@ -13,8 +13,7 @@ pub struct Gamenight { impl Display for Gamenight { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, r#" -Name: {} + write!(f, r#"Name: {} When: {}"#, self.name, self.start_time.format("%d-%m-%Y %H:%M")) } } \ No newline at end of file diff --git a/gamenight-cli/src/domain/mod.rs b/gamenight-cli/src/domain/mod.rs index 42a3f86..426bb68 100644 --- a/gamenight-cli/src/domain/mod.rs +++ b/gamenight-cli/src/domain/mod.rs @@ -1,2 +1,4 @@ pub mod gamenight; -pub mod user; \ No newline at end of file +pub mod user; +pub mod config; +pub mod participants; \ No newline at end of file diff --git a/gamenight-cli/src/domain/participants.rs b/gamenight-cli/src/domain/participants.rs new file mode 100644 index 0000000..98667d2 --- /dev/null +++ b/gamenight-cli/src/domain/participants.rs @@ -0,0 +1,12 @@ +use std::fmt::Display; + +use crate::domain::user::User; + +pub struct Participants<'a>(pub &'a Vec); + +impl<'a> Display for Participants<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let string_list: Vec = self.0.iter().map(|x| {format!("{}", x)}).collect(); + write!(f, "{}", string_list.join(", ")) + } +} \ No newline at end of file diff --git a/gamenight-cli/src/flows/add_gamenight.rs b/gamenight-cli/src/flows/add_gamenight.rs index 62bec6a..383f63c 100644 --- a/gamenight-cli/src/flows/add_gamenight.rs +++ b/gamenight-cli/src/flows/add_gamenight.rs @@ -35,8 +35,9 @@ impl<'a> Flow<'a> for AddGamenight { .to_utc() .to_rfc3339()); - post_gamenight(&state.configuration, Some(add_gamenight)).await?; + post_gamenight(&state.api_configuration, Some(add_gamenight)).await?; + clear_screen::clear(); Ok((FlowOutcome::Successful, state)) } } diff --git a/gamenight-cli/src/flows/connect.rs b/gamenight-cli/src/flows/connect.rs new file mode 100644 index 0000000..b4b54ac --- /dev/null +++ b/gamenight-cli/src/flows/connect.rs @@ -0,0 +1,99 @@ +use std::fmt::Display; + +use async_trait::async_trait; +use inquire::Text; + +use crate::{domain::config::{Config, Instance}, flows::{gamenight_menu::GamenightMenu, login::Login}}; + +use super::{Flow, FlowOutcome, FlowResult, GamenightState}; + +#[derive(Clone)] +pub struct Connect { + instance: Option +} + +impl Connect { + pub fn to(instance: Instance) -> Self { + Self { + instance: Some(instance) + } + } + + pub fn new() -> Self { + Self { + instance: None + } + } +} + +#[async_trait] +impl<'a> Flow<'a> for Connect { + async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> { + let mut instance = if let Some(instance) = self.instance.clone() { + instance + } else { + let mut name: String; + loop { + name = Text::new("Name this instance: ").prompt()?; + if state.gamenight_configuration.instances.iter().find(|x| x.name == name).is_none() { + break; + } + clear_screen::clear(); + println!("Name already in use, please provide a unique name"); + } + let url = Text::new("What is the server URL: ").prompt()?; + Instance::new(name, url) + }; + + let instance_name = instance.name.clone(); + + state.api_configuration.base_path = instance.url.clone(); + if let Some(token) = instance.token { + state.api_configuration.bearer_access_token = Some(token); + let result = gamenight_api_client_rs::apis::default_api::post_token(&state.api_configuration).await; + if let Ok(token) = result { + let instance = state.gamenight_configuration.instances.iter_mut().find(|x| x.name == instance_name).unwrap(); + instance.token = token.jwt_token.clone(); + state.api_configuration.bearer_access_token = token.jwt_token.clone(); + Config::save(&state.gamenight_configuration)?; + let gamenight_menu_flow = GamenightMenu::new(); + return gamenight_menu_flow.run(state).await + } + } + + let login_flow = Login::new(); + let (outcome, state) = login_flow.run(state).await?; + + if outcome == FlowOutcome::Successful { + if self.instance.is_none() { + instance.token = Some(state.api_configuration.bearer_access_token.clone().unwrap()); + state.gamenight_configuration.instances.push(instance.clone()); + } + else { + let instance = state.gamenight_configuration.instances.iter_mut().find(|x| x.name == instance_name).unwrap(); + instance.token = Some(state.api_configuration.bearer_access_token.clone().unwrap()); + } + + state.gamenight_configuration.last_instance = Some(instance_name); + + Config::save(&state.gamenight_configuration)?; + + let gamenight_menu_flow = GamenightMenu::new(); + gamenight_menu_flow.run(state).await + } + else { + Ok((outcome, state)) + } + } +} + +impl Display for Connect { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(instance) = &self.instance { + write!(f, "Connect to: {}", instance.name) + } else { + write!(f, "Connect") + } + + } +} \ No newline at end of file diff --git a/gamenight-cli/src/flows/exit.rs b/gamenight-cli/src/flows/exit.rs index 572375d..27bb4e2 100644 --- a/gamenight-cli/src/flows/exit.rs +++ b/gamenight-cli/src/flows/exit.rs @@ -14,6 +14,7 @@ impl Exit { #[async_trait] impl<'a> Flow<'a> for Exit { async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> { + clear_screen::clear(); Ok((FlowOutcome::Abort, state)) } } diff --git a/gamenight-cli/src/flows/main_menu.rs b/gamenight-cli/src/flows/gamenight_menu.rs similarity index 50% rename from gamenight-cli/src/flows/main_menu.rs rename to gamenight-cli/src/flows/gamenight_menu.rs index 0a8a30b..3d952af 100644 --- a/gamenight-cli/src/flows/main_menu.rs +++ b/gamenight-cli/src/flows/gamenight_menu.rs @@ -1,29 +1,35 @@ use inquire::{ui::RenderConfig, Select}; +use crate::flows::{add_gamenight::AddGamenight, exit::Exit, list_gamenights::ListGamenights}; + use super::*; #[derive(Clone)] -pub struct MainMenu { - pub menu: Vec Flow<'a> + Send>> +pub struct GamenightMenu { } -impl MainMenu { +impl GamenightMenu { pub fn new() -> Self { - MainMenu { - menu: vec![] - } + Self {} } } -unsafe impl Send for MainMenu { +unsafe impl Send for GamenightMenu { } #[async_trait] -impl<'a> Flow<'a> for MainMenu { +impl<'a> Flow<'a> for GamenightMenu { async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> { - let choice = Select::new("What would you like to do?", self.menu.clone()) + + let flows: Vec> = vec![ + Box::new(ListGamenights::new()), + Box::new(AddGamenight::new()), + Box::new(Exit::new()) + ]; + + let choice = Select::new("What would you like to do?", flows) .with_help_message("Select the action you want to take or quit the program") .with_render_config(RenderConfig { option_index_prefix: inquire::ui::IndexPrefix::Simple, @@ -31,11 +37,12 @@ impl<'a> Flow<'a> for MainMenu { }) .prompt_skippable()?; - handle_choice_option(&choice, self, state).await + clear_screen::clear(); + handle_choice_option(&choice, self, state).await } } -impl Display for MainMenu { +impl Display for GamenightMenu { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Main menu") } diff --git a/gamenight-cli/src/flows/join.rs b/gamenight-cli/src/flows/join.rs index 179cdc6..12cf4d5 100644 --- a/gamenight-cli/src/flows/join.rs +++ b/gamenight-cli/src/flows/join.rs @@ -22,7 +22,9 @@ impl Join { #[async_trait] impl<'a> Flow<'a> for Join { async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> { - let _ = join_post(&state.configuration, Some(GamenightId{gamenight_id: self.gamenight_id.to_string()})).await?; + let _ = join_post(&state.api_configuration, Some(GamenightId{gamenight_id: self.gamenight_id.to_string()})).await?; + + clear_screen::clear(); Ok((FlowOutcome::Successful, state)) } } diff --git a/gamenight-cli/src/flows/leave.rs b/gamenight-cli/src/flows/leave.rs index e8f58a2..3db820a 100644 --- a/gamenight-cli/src/flows/leave.rs +++ b/gamenight-cli/src/flows/leave.rs @@ -22,7 +22,9 @@ impl Leave { #[async_trait] impl<'a> Flow<'a> for Leave { async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> { - let _ = leave_post(&state.configuration, Some(GamenightId{gamenight_id: self.gamenight_id.to_string()})).await?; + let _ = leave_post(&state.api_configuration, Some(GamenightId{gamenight_id: self.gamenight_id.to_string()})).await?; + + clear_screen::clear(); Ok((FlowOutcome::Successful, state)) } } diff --git a/gamenight-cli/src/flows/list_gamenights.rs b/gamenight-cli/src/flows/list_gamenights.rs index 5030fb6..56c4950 100644 --- a/gamenight-cli/src/flows/list_gamenights.rs +++ b/gamenight-cli/src/flows/list_gamenights.rs @@ -22,7 +22,7 @@ impl ListGamenights { #[async_trait] impl<'a> Flow<'a> for ListGamenights { async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> { - let response = get_gamenights(&state.configuration).await?; + let response = get_gamenights(&state.api_configuration).await?; let mut view_flows: Vec + Send>> = vec![]; @@ -41,6 +41,7 @@ impl<'a> Flow<'a> for ListGamenights { let choice = Select::new("What gamenight would you like to view?", view_flows) .prompt_skippable()?; + clear_screen::clear(); handle_choice_option(&choice, self, state).await } } diff --git a/gamenight-cli/src/flows/login.rs b/gamenight-cli/src/flows/login.rs index cdf47bd..d4d4352 100644 --- a/gamenight-cli/src/flows/login.rs +++ b/gamenight-cli/src/flows/login.rs @@ -28,8 +28,10 @@ impl<'a> Flow<'a> for Login { let login = models::Login::new(username, password); let result = get_token(&configuration, Some(login)).await?; + + clear_screen::clear(); if let Some(token) = result.jwt_token { - state.configuration.bearer_access_token = Some(token); + state.api_configuration.bearer_access_token = Some(token); Ok((FlowOutcome::Successful, state)) } else { Err(FlowError{error: "Unexpected response".to_string()}) diff --git a/gamenight-cli/src/flows/main.rs b/gamenight-cli/src/flows/main.rs index a480fa3..46174f7 100644 --- a/gamenight-cli/src/flows/main.rs +++ b/gamenight-cli/src/flows/main.rs @@ -1,22 +1,19 @@ -use super::{exit::Exit, add_gamenight::AddGamenight, list_gamenights::ListGamenights, login::Login, main_menu::MainMenu, *}; +use std::fmt::Display; + +use inquire::{ui::RenderConfig, Select}; + +use crate::flows::{connect::Connect, exit::Exit, settings::Settings}; + +use super::*; #[derive(Clone)] pub struct Main { - login: Box Flow<'a>>, - main_menu: MainMenu } impl Main { pub fn new() -> Self { - let mut main_menu = MainMenu::new(); - main_menu.menu.push(Box::new(ListGamenights::new())); - main_menu.menu.push(Box::new(AddGamenight::new())); - main_menu.menu.push(Box::new(Exit::new())); - Self { - login: Box::new(Login::new()), - main_menu - } + Self {} } } @@ -29,9 +26,32 @@ impl Default for Main { #[async_trait] impl<'a> Flow<'a> for Main { async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> { - let (_outcome, state) = self.login.run(state).await?; - let (_outcome, state) = self.main_menu.run(state).await?; - Ok((FlowOutcome::Successful, state)) + + let config = Config::load()?; + state.gamenight_configuration = config; + + let mut choices: Vec + Send>> = vec![]; + + if let Some(last_instance) = &state.gamenight_configuration.last_instance { + if let Some(instance) = state.gamenight_configuration.instances.iter().find(|instance| {instance.name.eq(last_instance)}) { + choices.push(Box::new(Connect::to(instance.clone()))); + } + } + + choices.push(Box::new(Connect::new())); + choices.push(Box::new(Settings::new())); + choices.push(Box::new(Exit::new())); + + let choice = Select::new("What would you like to do?", choices) + .with_help_message("Select the action you want to take or quit the program") + .with_render_config(RenderConfig { + option_index_prefix: inquire::ui::IndexPrefix::Simple, + ..Default::default() + }) + .prompt_skippable()?; + + clear_screen::clear(); + handle_choice_option(&choice, self, state).await } } diff --git a/gamenight-cli/src/flows/mod.rs b/gamenight-cli/src/flows/mod.rs index 989e083..d0b1530 100644 --- a/gamenight-cli/src/flows/mod.rs +++ b/gamenight-cli/src/flows/mod.rs @@ -1,29 +1,36 @@ -use std::fmt::Display; +use std::{fmt::Display, num::ParseIntError}; use async_trait::async_trait; use chrono::ParseError; use gamenight_api_client_rs::apis::{configuration::Configuration, Error}; use inquire::InquireError; use dyn_clone::DynClone; +pub use clear_screen; + +use crate::domain::config::{Config, ConfigError}; pub mod main; mod login; -mod main_menu; +mod gamenight_menu; mod exit; mod list_gamenights; mod add_gamenight; mod view_gamenight; mod join; mod leave; +mod connect; +mod settings; pub struct GamenightState { - configuration: Configuration, + api_configuration: Configuration, + gamenight_configuration: Config, } impl GamenightState { pub fn new() -> Self{ Self { - configuration: Configuration::new() + api_configuration: Configuration::new(), + gamenight_configuration: Config::new() } } } @@ -71,6 +78,22 @@ impl From for FlowError { } } +impl From for FlowError { + fn from(value: ConfigError) -> Self { + Self { + error: value.0 + } + } +} + +impl From for FlowError { + fn from(value: ParseIntError) -> Self { + Self { + error: value.to_string() + } + } +} + #[derive(PartialEq)] pub enum FlowOutcome { Successful, diff --git a/gamenight-cli/src/flows/settings.rs b/gamenight-cli/src/flows/settings.rs new file mode 100644 index 0000000..a186cfd --- /dev/null +++ b/gamenight-cli/src/flows/settings.rs @@ -0,0 +1,29 @@ +use std::fmt::Display; + +use async_trait::async_trait; + +use super::*; + +#[derive(Clone)] +pub struct Settings { +} + +impl Settings { + pub fn new() -> Self { + Self { + } + } +} + +#[async_trait] +impl<'a> Flow<'a> for Settings { + async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> { + Ok((FlowOutcome::Successful, state)) + } +} + +impl Display for Settings { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Settings") + } +} \ No newline at end of file diff --git a/gamenight-cli/src/flows/view_gamenight.rs b/gamenight-cli/src/flows/view_gamenight.rs index 8d3c053..6b5d476 100644 --- a/gamenight-cli/src/flows/view_gamenight.rs +++ b/gamenight-cli/src/flows/view_gamenight.rs @@ -5,7 +5,7 @@ use jsonwebtoken::{decode, DecodingKey, Validation}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::{domain::{gamenight::Gamenight, user::User}, flows::{exit::Exit, join::Join, leave::Leave}}; +use crate::{domain::{gamenight::Gamenight, participants::Participants, user::User}, flows::{exit::Exit, join::Join, leave::Leave}}; use super::*; @@ -23,11 +23,7 @@ impl ViewGamenight { } -fn vec_user_to_usernames_string(users: &Vec) -> String { - - let string_list: Vec = users.iter().map(|x| {format!("{}", x)}).collect(); - string_list.join(", ") -} + #[derive(Debug, Serialize, Deserialize)] pub struct Claims { @@ -47,31 +43,31 @@ impl From for FlowError { impl<'a> Flow<'a> for ViewGamenight { async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> { - let participants = participants_get(&state.configuration, Some(GamenightId{gamenight_id: self.gamenight.id.to_string()})).await?; + let participants = participants_get(&state.api_configuration, Some(GamenightId{gamenight_id: self.gamenight.id.to_string()})).await?; let mut users = vec![]; for participant in participants.participants.iter() { - let user = user_get(&state.configuration, Some(UserId{user_id: participant.clone()})).await?; + let user = user_get(&state.api_configuration, Some(UserId{user_id: participant.clone()})).await?; users.push(User { id: Uuid::parse_str(&user.id)?, username: user.username }); } - println!("{}\nwho: {}", self.gamenight, vec_user_to_usernames_string(&users)); + println!("{}\nwho: {}", self.gamenight, Participants(&users)); let decoding_key = DecodingKey::from_secret(b""); let mut validation = Validation::default(); validation.insecure_disable_signature_validation(); let claims = decode::( - &state.configuration.bearer_access_token.as_ref().unwrap(), + &state.api_configuration.bearer_access_token.as_ref().unwrap(), &decoding_key, &validation)?.claims; let join_or_leave: Box + Send> = if users.iter().map(|x| {x.id}).find(|x| *x == claims.uid) != None { - Box::new(Leave::new(claims.uid)) + Box::new(Leave::new(self.gamenight.id)) } else { - Box::new(Join::new(claims.uid)) + Box::new(Join::new(self.gamenight.id)) }; let options: Vec + Send>> = vec![ @@ -81,6 +77,7 @@ impl<'a> Flow<'a> for ViewGamenight { let choice = Select::new("What do you want to do:", options) .prompt_skippable()?; + clear_screen::clear(); handle_choice_option(&choice, self, state).await } } diff --git a/gamenight-cli/src/main.rs b/gamenight-cli/src/main.rs index 7840e81..8b231f2 100644 --- a/gamenight-cli/src/main.rs +++ b/gamenight-cli/src/main.rs @@ -2,11 +2,13 @@ pub mod flows; pub mod domain; use flows::{main::Main, Flow, GamenightState}; +use clear_screen; #[tokio::main] async fn main() { let mut state = GamenightState::new(); let mainflow = Main::new(); + clear_screen::clear(); if let Err(x) = mainflow.run(&mut state).await { println!("{}", x.error); } diff --git a/gamenight-database/src/error.rs b/gamenight-database/src/error.rs index 2537132..692b5af 100644 --- a/gamenight-database/src/error.rs +++ b/gamenight-database/src/error.rs @@ -1,3 +1,4 @@ +#[derive(Debug)] pub struct DatabaseError(pub String); impl From for DatabaseError { diff --git a/gamenight-database/src/gamenight_participants.rs b/gamenight-database/src/gamenight_participants.rs index 96e5f65..4b2312c 100644 --- a/gamenight-database/src/gamenight_participants.rs +++ b/gamenight-database/src/gamenight_participants.rs @@ -28,7 +28,9 @@ pub fn insert_gamenight_participant(conn: &mut DbConnection, gp: GamenightPartic } pub fn delete_gamenight_participant(conn: &mut DbConnection, gp: GamenightParticipant) -> Result { - Ok(gamenight_participant::table - .filter(gamenight_participant::gamenight_id.eq(&gp.gamenight_id).and(gamenight_participant::user_id.eq(gp.user_id))) + Ok(diesel::delete(gamenight_participant::table + .filter(gamenight_participant::gamenight_id.eq(&gp.gamenight_id) + .and(gamenight_participant::user_id.eq(gp.user_id)))) .execute(conn)?) + } \ No newline at end of file