diff --git a/.vscode/launch.json b/.vscode/launch.json index 53f14fa..9459861 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -25,6 +25,7 @@ "QUEUE_BROKER": "bet.beteran", "CAPTCHA_SALT": "!#%&(24680qetuWRYI", "PASSWORD_SALT": "@$^*)13579wryipQETUO", + "JWT_SECRET": "!@$^*)13579wryipQETUO!", }, "args": [], "cwd": "${workspaceFolder}" @@ -51,6 +52,7 @@ "QUEUE_BROKER": "bet.beteran", "CAPTCHA_SALT": "!#%&(24680qetuWRYI", "PASSWORD_SALT": "@$^*)13579wryipQETUO", + "JWT_SECRET": "!@$^*)13579wryipQETUO!", }, "args": [], "cwd": "${workspaceFolder}" diff --git a/Cargo.toml b/Cargo.toml index dc147bb..44e21ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,16 +19,18 @@ diesel-derive-enum = { version = "1", features = ["postgres"] } futures = { version = "0", default-features = false, features = [ "async-await", ] } +jsonwebtoken = { version = "8" } nats = { version = "0" } prost = { version = "0" } rust-argon2 = { version = "1" } serde = { version = "1", features = ["derive"] } serde_json = { version = "1" } +time = { version = "0.3" } tokio = { version = "1", features = ["macros", "rt-multi-thread"] } tokio-cron-scheduler = { version = "0" } uuid = { version = "0", features = ["serde", "v4", "v5"] } -beteran-protobuf-rust = { git = "https://gitlab.loafle.net/bet/beteran-protobuf-rust.git", tag = "v0.1.24-snapshot" } -beteran-common-rust = { git = "https://gitlab.loafle.net/bet/beteran-common-rust.git", tag = "v0.1.4-snapshot" } +beteran-protobuf-rust = { git = "https://gitlab.loafle.net/bet/beteran-protobuf-rust.git", tag = "v0.1.25-snapshot" } +beteran-common-rust = { git = "https://gitlab.loafle.net/bet/beteran-common-rust.git", tag = "v0.1.5-snapshot" } [build-dependencies] diff --git a/src/compositions/identity/composition.rs b/src/compositions/identity/composition.rs new file mode 100644 index 0000000..134f5a6 --- /dev/null +++ b/src/compositions/identity/composition.rs @@ -0,0 +1,68 @@ +//! +//! + +use super::models; +use crate::repositories; +use time::{Duration, OffsetDateTime}; + +pub struct Composition { + jwt_secret: String, + site_repository: repositories::site::repository::Repository, +} + +impl std::fmt::Debug for Composition { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.debug_struct("Composition of members").finish() + } +} + +impl Composition { + /// + pub fn new(jwt_secret: String) -> Composition { + Composition { + jwt_secret, + site_repository: repositories::site::repository::Repository::new(), + } + } + + /// + pub fn get_token( + &self, + issuer: String, + session_id: String, + ) -> Result { + use jsonwebtoken::{encode, Algorithm, EncodingKey, Header}; + + let header = Header::new(Algorithm::HS512); + + let issued_at = OffsetDateTime::now_utc(); + let expiration_at = issued_at + Duration::days(1); + let claims = models::Claims { + iss: issuer, + iat: issued_at + .date() + .with_hms_milli(issued_at.hour(), issued_at.minute(), issued_at.second(), 0) + .unwrap() + .assume_utc(), + exp: expiration_at + .date() + .with_hms_milli( + expiration_at.hour(), + expiration_at.minute(), + expiration_at.second(), + 0, + ) + .unwrap() + .assume_utc(), + session_id, + }; + + let token = encode( + &header, + &claims, + &EncodingKey::from_secret(self.jwt_secret.as_bytes()), + )?; + + Ok(token) + } +} diff --git a/src/compositions/identity/mod.rs b/src/compositions/identity/mod.rs new file mode 100644 index 0000000..e0b4e90 --- /dev/null +++ b/src/compositions/identity/mod.rs @@ -0,0 +1,7 @@ +//! +//! + +/// +pub mod composition; +/// +pub mod models; diff --git a/src/compositions/identity/models.rs b/src/compositions/identity/models.rs new file mode 100644 index 0000000..108fab8 --- /dev/null +++ b/src/compositions/identity/models.rs @@ -0,0 +1,20 @@ +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; + +// #[derive(Debug, Serialize, Deserialize)] +// struct Claims { +// aud: String, // Optional. Audience +// exp: usize, // Required (validate_exp defaults to true in validation). Expiration time (as UTC timestamp) +// iat: usize, // Optional. Issued at (as UTC timestamp) +// iss: String, // Optional. Issuer +// nbf: usize, // Optional. Not Before (as UTC timestamp) +// sub: String, // Optional. Subject (whom token refers to) +// } + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub iss: String, + pub iat: OffsetDateTime, + pub exp: OffsetDateTime, + pub session_id: String, +} diff --git a/src/compositions/mod.rs b/src/compositions/mod.rs index 55c6a43..d1df18e 100644 --- a/src/compositions/mod.rs +++ b/src/compositions/mod.rs @@ -1,2 +1,3 @@ +pub mod identity; pub mod member; pub mod site; diff --git a/src/main.rs b/src/main.rs index e71899f..ae4c1dc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,6 +42,10 @@ async fn main() -> Result<(), Box> { Some(v) => v.into_string().unwrap(), None => "".to_string(), }; + let jwt_secret = match env::var_os("JWT_SECRET") { + Some(v) => v.into_string().unwrap(), + None => "".to_string(), + }; let manager = ConnectionManager::::new(url_db); let pool = Pool::builder() @@ -61,6 +65,7 @@ async fn main() -> Result<(), Box> { pool.clone(), captcha_salt.clone(), password_salt.clone(), + jwt_secret.clone(), ); println!("Server service [beteran-server-service] is started"); diff --git a/src/services/identity/service.rs b/src/services/identity/service.rs index 9209b24..4c6836e 100644 --- a/src/services/identity/service.rs +++ b/src/services/identity/service.rs @@ -20,6 +20,7 @@ pub struct Service<'a> { member_session_repository: repositories::member_session::repository::Repository, site_repository: repositories::site::repository::Repository, site_composition: compositions::site::composition::Composition, + identity_composition: compositions::identity::composition::Composition, argon2_config: argon2::Config<'a>, captcha_salt: String, password_salt: String, @@ -40,6 +41,7 @@ impl Service<'_> { pool: Pool>, captcha_salt: String, password_salt: String, + jwt_secret: String, ) -> Service<'static> { Service { connection_broker, @@ -49,6 +51,7 @@ impl Service<'_> { member_session_repository: repositories::member_session::repository::Repository::new(), site_repository: repositories::site::repository::Repository::new(), site_composition: compositions::site::composition::Composition::new(), + identity_composition: compositions::identity::composition::Composition::new(jwt_secret), argon2_config: argon2::Config::default(), captcha_salt, password_salt, @@ -546,13 +549,22 @@ impl Service<'_> { }) })?; + let access_token = self + .identity_composition + .get_token("Beteran".to_string(), session.id.to_string()) + .map_err(|e| { + bcr::error::rpc::Error::Server(bcr::error::rpc::Server { + code: bpr::protobuf::rpc::Error::SERVER_00, + message: format!("server {}", e), + data: None, + }) + })?; + message .respond( bpr::ss::member::identity::SigninResponse { error: None, - result: Some(bpr::ss::member::identity::signin_response::Result { - session_id: session.id.to_string(), - }), + result: Some(bpr::ss::member::identity::signin_response::Result { access_token }), } .encode_to_vec(), )