- Fixed panic when only one lesson in a class
- Split authentication and timetable handlers into connectors: -> keycloak_connector -> indiware_connector - Added standardized authentication error handler
This commit is contained in:
parent
1f8e4bbd8c
commit
1d7f75b6e2
185
src/indiware_connector.rs
Normal file
185
src/indiware_connector.rs
Normal file
@ -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<rocket::serde::json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<serde_json::value::Value> {
|
||||||
|
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<Timetable> {
|
||||||
|
let xml = get_timetable_xml().await;
|
||||||
|
let classes = get_timetable_xml_data().await;
|
||||||
|
let mut timetable: Vec<Timetable> = Vec::new();
|
||||||
|
for i in classes.iter() {
|
||||||
|
let mut courses: Vec<rocket::serde::json::Value> = 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<rocket::serde::json::Value> = 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<String> {
|
||||||
|
let classes = get_timetable_xml_data().await;
|
||||||
|
let mut class_list: Vec<String> = Vec::new();
|
||||||
|
for i in classes.iter() {
|
||||||
|
class_list.push(String::from(
|
||||||
|
i.as_object()
|
||||||
|
.unwrap()
|
||||||
|
.get("Kurz")
|
||||||
|
.unwrap()
|
||||||
|
.as_str()
|
||||||
|
.unwrap(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
class_list
|
||||||
|
}
|
211
src/keycloak_connector.rs
Normal file
211
src/keycloak_connector.rs
Normal file
@ -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<usize>,
|
||||||
|
pub refresh_expires_in: Option<usize>,
|
||||||
|
pub refresh_token: Option<String>,
|
||||||
|
pub scope: String,
|
||||||
|
pub session_state: Option<String>,
|
||||||
|
pub token_type: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct KeycloakUser {
|
||||||
|
sub: String,
|
||||||
|
email_verified: bool,
|
||||||
|
pub roles: Vec<Roles>,
|
||||||
|
name: String,
|
||||||
|
pub blacklist: Option<Vec<String>>,
|
||||||
|
pub groups: Vec<String>,
|
||||||
|
pub whitelist: Option<Vec<String>>,
|
||||||
|
pub preferred_username: String,
|
||||||
|
given_name: String,
|
||||||
|
family_name: String,
|
||||||
|
email: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn error_check(response: reqwest::Response) -> Result<reqwest::Response, KeycloakError> {
|
||||||
|
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<KeycloakAdminToken, KeycloakError> {
|
||||||
|
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<KeycloakUser, Box<dyn Error>> {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let resp = client
|
||||||
|
.get(config::KC_OPENID_USERINFO_ENDPOINT)
|
||||||
|
.header("Authorization", format!("Bearer {}", token))
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json::<KeycloakUser>()
|
||||||
|
.await?;
|
||||||
|
Ok(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_userinfo(
|
||||||
|
credentials: Json<Credentials>,
|
||||||
|
) -> Result<Json<KeycloakUser>, status::Unauthorized<String>> {
|
||||||
|
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::<String>(Some(token.outcome.1))),
|
||||||
|
};
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login(
|
||||||
|
credentials: Json<Credentials>,
|
||||||
|
) -> Result<Json<Token>, status::Unauthorized<String>> {
|
||||||
|
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::<String>(Some(token.outcome.1))),
|
||||||
|
};
|
||||||
|
result
|
||||||
|
}
|
442
src/main.rs
442
src/main.rs
@ -4,49 +4,33 @@ extern crate rocket;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate diesel;
|
extern crate diesel;
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
mod indiware_connector;
|
||||||
|
mod keycloak_connector;
|
||||||
mod schema;
|
mod schema;
|
||||||
|
|
||||||
extern crate reqwest;
|
extern crate reqwest;
|
||||||
extern crate serde;
|
extern crate serde;
|
||||||
|
extern crate serde_json;
|
||||||
|
|
||||||
use crate::schema::timetable;
|
use indiware_connector as timetable_connector;
|
||||||
use diesel::{Insertable, Queryable};
|
use jsonwebtoken::{decode, DecodingKey, Validation};
|
||||||
use quickxml_to_serde::{xml_string_to_json, Config};
|
use keycloak_connector::KeycloakUser;
|
||||||
use rocket::serde::json::{json, Json};
|
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 rocket_sync_db_pools::{database, diesel::PgConnection};
|
||||||
use serde_derive::{Deserialize, Serialize};
|
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")]
|
#[database("timetable")]
|
||||||
struct DbConn(PgConnection);
|
pub 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<rocket::serde::json::Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct Credentials {
|
pub struct Credentials {
|
||||||
user: String,
|
user: String,
|
||||||
password: String,
|
password: String,
|
||||||
otp: String,
|
otp: String,
|
||||||
@ -54,7 +38,7 @@ struct Credentials {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
enum Roles {
|
pub enum Roles {
|
||||||
Student,
|
Student,
|
||||||
Teacher,
|
Teacher,
|
||||||
Admin,
|
Admin,
|
||||||
@ -80,14 +64,8 @@ enum TokenStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct TokenOutcome {
|
pub struct Token {
|
||||||
status: TokenStatus,
|
outcome: (TokenStatus, String),
|
||||||
info: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
struct Token {
|
|
||||||
outcome: TokenOutcome,
|
|
||||||
token: String,
|
token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,34 +77,6 @@ enum ApiKeyError {
|
|||||||
Invalid,
|
Invalid,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct KeycloakAdminToken {
|
|
||||||
access_token: String,
|
|
||||||
expires_in: usize,
|
|
||||||
#[serde(rename = "not-before-policy")]
|
|
||||||
not_before_policy: Option<usize>,
|
|
||||||
refresh_expires_in: Option<usize>,
|
|
||||||
refresh_token: Option<String>,
|
|
||||||
scope: String,
|
|
||||||
session_state: Option<String>,
|
|
||||||
token_type: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
struct KeycloakUser {
|
|
||||||
sub: String,
|
|
||||||
email_verified: bool,
|
|
||||||
roles: Vec<Roles>,
|
|
||||||
name: String,
|
|
||||||
blacklist: Option<Vec<String>>,
|
|
||||||
groups: Vec<String>,
|
|
||||||
whitelist: Option<Vec<String>>,
|
|
||||||
preferred_username: String,
|
|
||||||
given_name: String,
|
|
||||||
family_name: String,
|
|
||||||
email: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rocket::async_trait]
|
#[rocket::async_trait]
|
||||||
impl<'r> FromRequest<'r> for ApiKey<'r> {
|
impl<'r> FromRequest<'r> for ApiKey<'r> {
|
||||||
type Error = ApiKeyError;
|
type Error = ApiKeyError;
|
||||||
@ -134,7 +84,10 @@ impl<'r> FromRequest<'r> for ApiKey<'r> {
|
|||||||
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||||
/// Returns true if `key` is a valid API key string.
|
/// Returns true if `key` is a valid API key string.
|
||||||
fn is_valid(key: &str) -> bool {
|
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::<Claims>(
|
let token = decode::<Claims>(
|
||||||
key,
|
key,
|
||||||
&DecodingKey::from_secret(config::JWT_SECRET.as_ref()),
|
&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 {
|
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 =
|
let standard_permissions =
|
||||||
vec![String::from("/api/timetable"), String::from("/api/classes")];
|
vec![String::from("/api/timetable"), String::from("/api/classes")];
|
||||||
let student_permissions: Vec<String> = vec![];
|
let student_permissions: Vec<String> = vec![];
|
||||||
@ -189,348 +145,42 @@ impl<'r> FromRequest<'r> for ApiKey<'r> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn error_check(response: reqwest::Response) -> Result<reqwest::Response, KeycloakError> {
|
#[post("/", data = "<credentials>")]
|
||||||
if !response.status().is_success() {
|
async fn get_userinfo(
|
||||||
let status = response.status().into();
|
credentials: Json<Credentials>,
|
||||||
let text = response.text().await?;
|
) -> Result<Json<KeycloakUser>, status::Unauthorized<String>> {
|
||||||
return Err(KeycloakError::HttpFailure {
|
keycloak_connector::get_userinfo(credentials).await
|
||||||
status,
|
|
||||||
body: serde_json::from_str(&text).ok(),
|
|
||||||
text,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_keycloak_token(
|
|
||||||
user: String,
|
|
||||||
password: String,
|
|
||||||
otp: String,
|
|
||||||
) -> Result<KeycloakAdminToken, KeycloakError> {
|
|
||||||
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<KeycloakUser, Box<dyn Error>> {
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
let resp = client
|
|
||||||
.get(config::KC_OPENID_USERINFO_ENDPOINT)
|
|
||||||
.header("Authorization", format!("Bearer {}", token))
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.json::<KeycloakUser>()
|
|
||||||
.await?;
|
|
||||||
Ok(resp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/", data = "<credentials>")]
|
#[post("/", data = "<credentials>")]
|
||||||
async fn get_userinfo(credentials: Json<Credentials>) -> Result<Json<KeycloakUser>, status::Unauthorized<()>> {
|
async fn login(
|
||||||
let credentials = credentials.into_inner();
|
credentials: Json<Credentials>,
|
||||||
let keycloak_resp = get_keycloak_token(
|
) -> Result<Json<Token>, status::Unauthorized<String>> {
|
||||||
credentials.user.clone(),
|
keycloak_connector::login(credentials).await
|
||||||
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 = "<credentials>")]
|
|
||||||
async fn login(credentials: Json<Credentials>) -> Json<Token> {
|
|
||||||
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<serde_json::value::Value> {
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
async fn get_timetable(_conn: DbConn, _key: ApiKey<'_>) -> Json<Vec<Timetable>> {
|
async fn get_timetable(
|
||||||
let xml = get_timetable_xml().await;
|
conn: DbConn,
|
||||||
let classes = get_timetable_xml_data().await;
|
_key: ApiKey<'_>,
|
||||||
let mut timetable: Vec<Timetable> = Vec::new();
|
) -> Json<Vec<timetable_connector::Timetable>> {
|
||||||
for i in classes.iter() {
|
let timetable = timetable_connector::get_timetable(conn).await;
|
||||||
let mut courses: Vec<rocket::serde::json::Value> = 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)
|
|
||||||
}
|
|
||||||
Json::from(timetable)
|
Json::from(timetable)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/<class>")]
|
#[get("/<class>")]
|
||||||
async fn get_class_timetable(
|
async fn get_class_timetable(
|
||||||
_conn: DbConn,
|
conn: DbConn,
|
||||||
class: String,
|
class: String,
|
||||||
_key: ApiKey<'_>,
|
_key: ApiKey<'_>,
|
||||||
) -> Json<TimetableData> {
|
) -> Json<timetable_connector::TimetableData> {
|
||||||
let classes = get_timetable_xml_data().await;
|
let timetable = timetable_connector::get_class_timetable(conn, class).await;
|
||||||
let courses: Vec<rocket::serde::json::Value> = Vec::new();
|
Json::from(timetable)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
async fn get_classes(_key: ApiKey<'_>) -> Json<Vec<String>> {
|
async fn get_classes(_key: ApiKey<'_>) -> Json<Vec<String>> {
|
||||||
let classes = get_timetable_xml_data().await;
|
let class_list = timetable_connector::get_classes().await;
|
||||||
let mut class_list: Vec<String> = Vec::new();
|
|
||||||
for i in classes.iter() {
|
|
||||||
class_list.push(
|
|
||||||
i.as_object()
|
|
||||||
.unwrap()
|
|
||||||
.get("Kurz")
|
|
||||||
.unwrap()
|
|
||||||
.as_str()
|
|
||||||
.unwrap()
|
|
||||||
.replace("/", "_"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Json::from(class_list)
|
Json::from(class_list)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user