diff --git a/src/indiware_connector.rs b/src/indiware_connector.rs new file mode 100644 index 0000000..892a99b --- /dev/null +++ b/src/indiware_connector.rs @@ -0,0 +1,185 @@ +use crate::config; +use crate::schema::timetable; +use crate::DbConn; +use diesel::{Insertable, Queryable}; +use quickxml_to_serde::{xml_string_to_json, Config}; +use serde_derive::{Deserialize, Serialize}; +use serde_json::json; + +#[derive(Queryable, Serialize, Insertable, Deserialize)] +#[table_name = "timetable"] +pub struct Timetable { + pub date: String, + pub updated: String, + pub class: String, + pub timetable_data: serde_json::Value, +} + +#[derive(Serialize, Deserialize)] +pub struct TimetableData { + pub count: usize, + pub courses: Vec, +} + +async fn get_timetable_xml() -> serde_json::value::Value { + let client = reqwest::Client::new(); + let resp = client + .get(config::TIMETABLE_URL) + .basic_auth(config::TIMETABLE_USER, config::TIMETABLE_PASSWORD) + .send() + .await + .unwrap() + .text() + .await + .unwrap(); + xml_string_to_json(resp, &Config::new_with_defaults()).unwrap() +} + +async fn get_timetable_xml_data() -> Vec { + let xml = get_timetable_xml().await; + let classes = xml + .as_object() + .unwrap() + .get("VpMobil") + .unwrap() + .get("Klassen") + .unwrap() + .get("Kl") + .unwrap() + .as_array() + .unwrap(); + classes.to_owned() +} + +pub async fn get_timetable(_conn: DbConn) -> Vec { + let xml = get_timetable_xml().await; + let classes = get_timetable_xml_data().await; + let mut timetable: Vec = Vec::new(); + for i in classes.iter() { + let mut courses: Vec = Vec::new(); + let nothing = json!([""]); + let std = i + .as_object() + .unwrap() + .get("Pl") + .unwrap() + .as_object() + .unwrap() + .get("Std") + .unwrap_or(¬hing); + let mut plan = vec![]; + if std.is_array() { + plan.extend(std.as_array().unwrap().iter().cloned()) + } else if std.is_object() { + plan.push(std.clone()) + } + for i in &plan { + if i.as_object() != None { + courses.push(i.to_owned()); + } else { + dbg!("Failed: {:?}", &i); + } + } + let response = TimetableData { + count: plan.len(), + courses, + }; + let timetable_element = Timetable { + date: String::from( + xml.as_object() + .unwrap() + .get("VpMobil") + .unwrap() + .get("Kopf") + .unwrap() + .get("DatumPlan") + .unwrap() + .as_str() + .unwrap(), + ), + updated: String::from( + xml.as_object() + .unwrap() + .get("VpMobil") + .unwrap() + .get("Kopf") + .unwrap() + .get("zeitstempel") + .unwrap() + .as_str() + .unwrap(), + ), + class: String::from( + i.as_object() + .unwrap() + .get("Kurz") + .unwrap() + .as_str() + .unwrap(), + ), + timetable_data: serde_json::from_str(&json!(response).to_string()).unwrap(), + }; + timetable.push(timetable_element) + } + timetable +} + +pub async fn get_class_timetable(_conn: DbConn, class: String) -> TimetableData { + let classes = get_timetable_xml_data().await; + let courses: Vec = Vec::new(); + let mut response = TimetableData { count: 0, courses }; + for i in classes.iter() { + if i.as_object() + .unwrap() + .get("Kurz") + .unwrap() + .as_str() + .unwrap() + .replace("/", "_") + == class + { + let nothing = json!([""]); + let std = i + .as_object() + .unwrap() + .get("Pl") + .unwrap() + .as_object() + .unwrap() + .get("Std") + .unwrap_or(¬hing); + let mut plan = vec![]; + if std.is_array() { + plan.extend(std.as_array().unwrap().iter().cloned()) + } else if std.is_object() { + plan.push(std.clone()) + } + response.count = plan.len(); + for i in plan { + if i.as_object() != None { + response.courses.push(i.to_owned()); + } else { + dbg!("Failed: {:?}", &i); + } + } + break; + } + } + response +} + +pub async fn get_classes() -> Vec { + let classes = get_timetable_xml_data().await; + let mut class_list: Vec = Vec::new(); + for i in classes.iter() { + class_list.push(String::from( + i.as_object() + .unwrap() + .get("Kurz") + .unwrap() + .as_str() + .unwrap(), + )) + } + class_list +} diff --git a/src/keycloak_connector.rs b/src/keycloak_connector.rs new file mode 100644 index 0000000..842045e --- /dev/null +++ b/src/keycloak_connector.rs @@ -0,0 +1,211 @@ +extern crate reqwest; + +use crate::config; +use crate::{Claims, Credentials, Roles, Token, TokenStatus}; + +use jsonwebtoken::{encode, EncodingKey, Header}; +use keycloak::KeycloakError; +use rocket::{response::status, serde::json::Json}; +use serde_derive::{Deserialize, Serialize}; +use serde_json::json; +use std::error::Error; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use time::OffsetDateTime; + +#[derive(Debug, Deserialize, Serialize)] +pub struct KeycloakAdminToken { + pub access_token: String, + pub expires_in: usize, + #[serde(rename = "not-before-policy")] + pub not_before_policy: Option, + pub refresh_expires_in: Option, + pub refresh_token: Option, + pub scope: String, + pub session_state: Option, + pub token_type: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct KeycloakUser { + sub: String, + email_verified: bool, + pub roles: Vec, + name: String, + pub blacklist: Option>, + pub groups: Vec, + pub whitelist: Option>, + pub preferred_username: String, + given_name: String, + family_name: String, + email: String, +} + +async fn error_check(response: reqwest::Response) -> Result { + if !response.status().is_success() { + let status = response.status().into(); + let text = response.text().await?; + return Err(KeycloakError::HttpFailure { + status, + body: serde_json::from_str(&text).ok(), + text, + }); + } + Ok(response) +} + +pub async fn get_keycloak_token( + user: String, + password: String, + otp: String, +) -> Result { + let client = reqwest::Client::new(); + let params = [ + ("username", user), + ("password", password), + ("totp", otp), + ("client_id", config::KC_CLIENT_ID.to_string()), + ("grant_type", String::from("password")), + ]; + let resp = client + .post(config::KC_OPENID_TOKEN_ENDPOINT) + .form(¶ms) + .send() + .await?; + Ok(error_check(resp).await?.json().await?) +} + +pub async fn get_keycloak_userinfo(token: String) -> Result> { + let client = reqwest::Client::new(); + let resp = client + .get(config::KC_OPENID_USERINFO_ENDPOINT) + .header("Authorization", format!("Bearer {}", token)) + .send() + .await? + .json::() + .await?; + Ok(resp) +} + +pub async fn get_userinfo( + credentials: Json, +) -> Result, status::Unauthorized> { + let credentials = credentials.into_inner(); + let keycloak_resp = get_keycloak_token( + credentials.user.clone(), + credentials.password.clone(), + credentials.otp.clone(), + ) + .await; + let token = match keycloak_resp { + Ok(token) => Token { + outcome: (TokenStatus::Success, String::new()), + token: token.access_token, + }, + Err(e) => { + let outcome = match e { + KeycloakError::ReqwestFailure(f) => (TokenStatus::HttpError, f.to_string()), + KeycloakError::HttpFailure { + status: _s, + body: _b, + text: t, + } => ( + TokenStatus::KeycloakError, + String::from( + serde_json::from_str(&t[..]).unwrap_or_else( + |_| json![{"error_description": "No error description"}], + )["error_description"] + .as_str() + .unwrap(), + ), + ), + }; + Token { + outcome, + token: String::new(), + } + } + }; + let result = match token.outcome.0 { + TokenStatus::Success => Ok(Json( + get_keycloak_userinfo(token.token.clone()).await.unwrap(), + )), + _ => Err(status::Unauthorized::(Some(token.outcome.1))), + }; + + result +} + +pub async fn login( + credentials: Json, +) -> Result, status::Unauthorized> { + let credentials = credentials.into_inner(); + let keycloak_resp = get_keycloak_token( + credentials.user.clone(), + credentials.password.clone(), + credentials.otp.clone(), + ) + .await; + let token = match keycloak_resp { + Ok(token) => Token { + outcome: (TokenStatus::Success, String::new()), + token: token.access_token, + }, + Err(e) => { + let outcome = match e { + KeycloakError::ReqwestFailure(f) => (TokenStatus::HttpError, f.to_string()), + KeycloakError::HttpFailure { + status: _s, + body: _b, + text: t, + } => ( + TokenStatus::KeycloakError, + String::from( + serde_json::from_str(&t[..]).unwrap_or_else( + |_| json![{"error_description": "No error description"}], + )["error_description"] + .as_str() + .unwrap(), + ), + ), + }; + Token { + outcome, + token: String::new(), + } + } + }; + + let result = match token.outcome.0 { + TokenStatus::Success => { + let userinfo = get_keycloak_userinfo(token.token.clone()).await.unwrap(); + let system_time = OffsetDateTime::now_utc(); + let datetime = system_time.format("%d/%m/%Y %T"); + let my_claims = Claims { + iss: String::from(config::JWT_ISSUER), + user: userinfo.preferred_username, + roles: userinfo.roles, + groups: userinfo.groups, + blacklist: userinfo.blacklist.unwrap_or_default(), + whitelist: userinfo.whitelist.unwrap_or_default(), + jid: (credentials.devid + "@" + &datetime), + exp: SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs() + + Duration::from_secs(31536000).as_secs(), + }; + println!("{:?}", SystemTime::now()); + let jwt = encode( + &Header::default(), + &my_claims, + &EncodingKey::from_secret(config::JWT_SECRET.as_ref()), + ); + Ok(Json(Token { + outcome: (TokenStatus::Success, String::new()), + token: jwt.unwrap(), + })) + } + _ => Err(status::Unauthorized::(Some(token.outcome.1))), + }; + result +} diff --git a/src/main.rs b/src/main.rs index b110f48..5a5de0a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,49 +4,33 @@ extern crate rocket; #[macro_use] extern crate diesel; +mod config; +mod indiware_connector; +mod keycloak_connector; mod schema; extern crate reqwest; extern crate serde; +extern crate serde_json; -use crate::schema::timetable; -use diesel::{Insertable, Queryable}; -use quickxml_to_serde::{xml_string_to_json, Config}; -use rocket::serde::json::{json, Json}; +use indiware_connector as timetable_connector; +use jsonwebtoken::{decode, DecodingKey, Validation}; +use keycloak_connector::KeycloakUser; +use rocket::{ + fs::{relative, FileServer}, + http::Status, + request::{FromRequest, Outcome, Request}, + response::status, + serde::json::Json, +}; use rocket_sync_db_pools::{database, diesel::PgConnection}; use serde_derive::{Deserialize, Serialize}; -extern crate serde_json; -mod config; -use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; -use keycloak::KeycloakError; -use rocket::fs::{relative, FileServer}; -use rocket::http::Status; -use rocket::response::status; -use rocket::request::{FromRequest, Outcome, Request}; -use std::error::Error; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use time::OffsetDateTime; #[database("timetable")] -struct DbConn(PgConnection); - -#[derive(Queryable, Serialize, Insertable, Deserialize)] -#[table_name = "timetable"] -struct Timetable { - date: String, - updated: String, - class: String, - timetable_data: serde_json::Value, -} - -#[derive(Serialize, Deserialize)] -struct TimetableData { - count: usize, - courses: Vec, -} +pub struct DbConn(PgConnection); #[derive(Debug, Serialize, Deserialize)] -struct Credentials { +pub struct Credentials { user: String, password: String, otp: String, @@ -54,7 +38,7 @@ struct Credentials { } #[derive(Debug, Serialize, Deserialize)] -enum Roles { +pub enum Roles { Student, Teacher, Admin, @@ -80,14 +64,8 @@ enum TokenStatus { } #[derive(Debug, Serialize, Deserialize)] -struct TokenOutcome { - status: TokenStatus, - info: String, -} - -#[derive(Debug, Serialize, Deserialize)] -struct Token { - outcome: TokenOutcome, +pub struct Token { + outcome: (TokenStatus, String), token: String, } @@ -99,34 +77,6 @@ enum ApiKeyError { Invalid, } -#[derive(Debug, Deserialize, Serialize)] -pub struct KeycloakAdminToken { - access_token: String, - expires_in: usize, - #[serde(rename = "not-before-policy")] - not_before_policy: Option, - refresh_expires_in: Option, - refresh_token: Option, - scope: String, - session_state: Option, - token_type: String, -} - -#[derive(Debug, Serialize, Deserialize)] -struct KeycloakUser { - sub: String, - email_verified: bool, - roles: Vec, - name: String, - blacklist: Option>, - groups: Vec, - whitelist: Option>, - preferred_username: String, - given_name: String, - family_name: String, - email: String, -} - #[rocket::async_trait] impl<'r> FromRequest<'r> for ApiKey<'r> { type Error = ApiKeyError; @@ -134,7 +84,10 @@ impl<'r> FromRequest<'r> for ApiKey<'r> { async fn from_request(req: &'r Request<'_>) -> Outcome { /// Returns true if `key` is a valid API key string. fn is_valid(key: &str) -> bool { - let validation = Validation { validate_exp: false, ..Default::default() }; + let validation = Validation { + validate_exp: false, + ..Default::default() + }; let token = decode::( key, &DecodingKey::from_secret(config::JWT_SECRET.as_ref()), @@ -144,7 +97,10 @@ impl<'r> FromRequest<'r> for ApiKey<'r> { } fn has_permissions(key: &str, uri: &str) -> bool { - let validation = Validation { validate_exp: false, ..Default::default() }; + let validation = Validation { + validate_exp: false, + ..Default::default() + }; let standard_permissions = vec![String::from("/api/timetable"), String::from("/api/classes")]; let student_permissions: Vec = vec![]; @@ -189,348 +145,42 @@ impl<'r> FromRequest<'r> for ApiKey<'r> { } } -async fn error_check(response: reqwest::Response) -> Result { - if !response.status().is_success() { - let status = response.status().into(); - let text = response.text().await?; - return Err(KeycloakError::HttpFailure { - status, - body: serde_json::from_str(&text).ok(), - text, - }); - } - Ok(response) -} - -async fn get_keycloak_token( - user: String, - password: String, - otp: String, -) -> Result { - let client = reqwest::Client::new(); - let params = [ - ("username", user), - ("password", password), - ("totp", otp), - ("client_id", config::KC_CLIENT_ID.to_string()), - ("grant_type", String::from("password")), - ]; - let resp = client - .post(config::KC_OPENID_TOKEN_ENDPOINT) - .form(¶ms) - .send() - .await?; - Ok(error_check(resp).await?.json().await?) -} - -async fn get_keycloak_userinfo(token: String) -> Result> { - let client = reqwest::Client::new(); - let resp = client - .get(config::KC_OPENID_USERINFO_ENDPOINT) - .header("Authorization", format!("Bearer {}", token)) - .send() - .await? - .json::() - .await?; - Ok(resp) +#[post("/", data = "")] +async fn get_userinfo( + credentials: Json, +) -> Result, status::Unauthorized> { + keycloak_connector::get_userinfo(credentials).await } #[post("/", data = "")] -async fn get_userinfo(credentials: Json) -> Result, status::Unauthorized<()>> { - let credentials = credentials.into_inner(); - let keycloak_resp = get_keycloak_token( - credentials.user.clone(), - credentials.password.clone(), - credentials.otp.clone(), - ) - .await; - let token = match keycloak_resp { - Ok(token) => Token { - outcome: TokenOutcome { - status: TokenStatus::Success, - info: String::new(), - }, - token: token.access_token, - }, - Err(e) => { - let outcome = match e { - KeycloakError::ReqwestFailure(f) => TokenOutcome { - status: TokenStatus::HttpError, - info: f.to_string(), - }, - KeycloakError::HttpFailure { - status: _s, - body: _b, - text: t, - } => TokenOutcome { - status: TokenStatus::KeycloakError, - info: String::from( - serde_json::from_str(&t[..]) - .unwrap_or_else(|_| json![{"error_description": "No error description"}]) - ["error_description"] - .as_str() - .unwrap(), - ), - }, - }; - Token { - outcome, - token: String::new(), - } - } - }; - let outcome = match token.outcome.status { - TokenStatus::Success => Ok(Json(get_keycloak_userinfo(token.token.clone()).await.unwrap())), - _ => Err(status::Unauthorized::<()>(None)) - }; - - outcome -} - - -#[post("/", data = "")] -async fn login(credentials: Json) -> Json { - let credentials = credentials.into_inner(); - let keycloak_resp = get_keycloak_token( - credentials.user.clone(), - credentials.password.clone(), - credentials.otp.clone(), - ) - .await; - let mut token = match keycloak_resp { - Ok(token) => Token { - outcome: TokenOutcome { - status: TokenStatus::Success, - info: String::new(), - }, - token: token.access_token, - }, - Err(e) => { - let outcome = match e { - KeycloakError::ReqwestFailure(f) => TokenOutcome { - status: TokenStatus::HttpError, - info: f.to_string(), - }, - KeycloakError::HttpFailure { - status: _s, - body: _b, - text: t, - } => TokenOutcome { - status: TokenStatus::KeycloakError, - info: String::from( - serde_json::from_str(&t[..]) - .unwrap_or_else(|_| json![{"error_description": "No error description"}]) - ["error_description"] - .as_str() - .unwrap(), - ), - }, - }; - Token { - outcome, - token: String::new(), - } - } - }; - - if let TokenStatus::Success = token.outcome.status { - let userinfo = get_keycloak_userinfo(token.token.clone()).await.unwrap(); - let system_time = OffsetDateTime::now_utc(); - let datetime = system_time.format("%d/%m/%Y %T"); - let my_claims = Claims { - iss: String::from(config::JWT_ISSUER), - user: userinfo.preferred_username, - roles: userinfo.roles, - groups: userinfo.groups, - blacklist: userinfo.blacklist.unwrap_or_default(), - whitelist: userinfo.whitelist.unwrap_or_default(), - jid: (credentials.devid + "@" + &datetime), - exp: SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs() - + Duration::from_secs(31536000).as_secs(), - }; - println!("{:?}", SystemTime::now()); - let jwt = encode( - &Header::default(), - &my_claims, - &EncodingKey::from_secret(config::JWT_SECRET.as_ref()), - ); - token = Token { - outcome: TokenOutcome { - status: TokenStatus::Success, - info: String::new(), - }, - token: jwt.unwrap(), - }; - } - Json(token) -} - -async fn get_timetable_xml() -> serde_json::value::Value { - let client = reqwest::Client::new(); - let resp = client - .get(config::TIMETABLE_URL) - .basic_auth(config::TIMETABLE_USER, config::TIMETABLE_PASSWORD) - .send() - .await - .unwrap() - .text() - .await - .unwrap(); - xml_string_to_json(resp, &Config::new_with_defaults()).unwrap() -} - -async fn get_timetable_xml_data() -> Vec { - let xml = get_timetable_xml().await; - let classes = xml - .as_object() - .unwrap() - .get("VpMobil") - .unwrap() - .get("Klassen") - .unwrap() - .get("Kl") - .unwrap() - .as_array() - .unwrap(); - classes.to_owned() +async fn login( + credentials: Json, +) -> Result, status::Unauthorized> { + keycloak_connector::login(credentials).await } #[get("/")] -async fn get_timetable(_conn: DbConn, _key: ApiKey<'_>) -> Json> { - let xml = get_timetable_xml().await; - let classes = get_timetable_xml_data().await; - let mut timetable: Vec = Vec::new(); - for i in classes.iter() { - let mut courses: Vec = Vec::new(); - let nothing = json!([""]); - let plan = i - .as_object() - .unwrap() - .get("Pl") - .unwrap() - .as_object() - .unwrap() - .get("Std") - .unwrap_or(¬hing) - .as_array() - .unwrap(); - for i in plan { - if i.as_object() != None { - courses.push(i.to_owned()); - } else { - dbg!("Failed: {:?}", &i); - } - } - let response = TimetableData { - count: plan.len(), - courses, - }; - let timetable_element = Timetable { - date: String::from( - xml.as_object() - .unwrap() - .get("VpMobil") - .unwrap() - .get("Kopf") - .unwrap() - .get("DatumPlan") - .unwrap() - .as_str() - .unwrap(), - ), - updated: String::from( - xml.as_object() - .unwrap() - .get("VpMobil") - .unwrap() - .get("Kopf") - .unwrap() - .get("zeitstempel") - .unwrap() - .as_str() - .unwrap(), - ), - class: String::from( - i.as_object() - .unwrap() - .get("Kurz") - .unwrap() - .as_str() - .unwrap(), - ), - timetable_data: serde_json::from_str(&json!(response).to_string()).unwrap(), - }; - timetable.push(timetable_element) - } +async fn get_timetable( + conn: DbConn, + _key: ApiKey<'_>, +) -> Json> { + let timetable = timetable_connector::get_timetable(conn).await; Json::from(timetable) } #[get("/")] async fn get_class_timetable( - _conn: DbConn, + conn: DbConn, class: String, _key: ApiKey<'_>, -) -> Json { - let classes = get_timetable_xml_data().await; - let courses: Vec = Vec::new(); - let mut response = TimetableData { - count: 0, - courses, - }; - for i in classes.iter() { - if i.as_object() - .unwrap() - .get("Kurz") - .unwrap() - .as_str() - .unwrap() - .replace("/", "_") - == class - { - let nothing = json!([""]); - let plan = i - .as_object() - .unwrap() - .get("Pl") - .unwrap() - .as_object() - .unwrap() - .get("Std") - .unwrap_or(¬hing) - .as_array() - .unwrap(); - response.count = plan.len(); - for i in plan { - if i.as_object() != None { - response.courses.push(i.to_owned()); - } else { - dbg!("Failed: {:?}", &i); - } - } - break; - } - } - Json::from(response) +) -> Json { + let timetable = timetable_connector::get_class_timetable(conn, class).await; + Json::from(timetable) } #[get("/")] async fn get_classes(_key: ApiKey<'_>) -> Json> { - let classes = get_timetable_xml_data().await; - let mut class_list: Vec = Vec::new(); - for i in classes.iter() { - class_list.push( - i.as_object() - .unwrap() - .get("Kurz") - .unwrap() - .as_str() - .unwrap() - .replace("/", "_"), - ) - } + let class_list = timetable_connector::get_classes().await; Json::from(class_list) }