Change authentication/authorization flow, code cleanup, severa fixes
This commit is contained in:
parent
5db7f250e3
commit
31486a9d5e
@ -1,19 +1,16 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "api"
|
name = "api"
|
||||||
version = "1.0.0"
|
version = "2.0.0-alpha.1"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = ["Denys Konovalov <denys.konovalov@protonmail.com>"]
|
authors = ["Denys Konovalov <kontakt@denyskon.de>"]
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rocket = { version = "0.5.0-rc.1", features = ["json"] }
|
rocket = { version = "0.5.0-rc.2", features = ["json"] }
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
reqwest = { version="0.11", features = ["json"] }
|
reqwest = { version="0.11", features = ["json"] }
|
||||||
quickxml_to_serde = "0.5"
|
quickxml_to_serde = "0.5"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
jsonwebtoken = "8.1"
|
|
||||||
time = "0.3"
|
|
||||||
chrono = "0.4"
|
|
||||||
|
12
README.md
12
README.md
@ -1,7 +1,7 @@
|
|||||||
# meincantor-api
|
# meincantor-api
|
||||||
The API backend for the GCG.MeinCantor school platform built with Rocket.rs in Rust.
|
The API backend for the GCG.MeinCantor school platform built with Rocket.rs in Rust.
|
||||||
|
|
||||||
It includes a plugin for receiving data from Indiware Mobil and a Keycloak authentication extension.
|
It includes a plugin for receiving data from Indiware Mobil and an OpenID Connect authentication extension.
|
||||||
|
|
||||||
See the repository of the [main application](https://git.cantorgymnasium.de/cantortechnik/meincantor-app) for additional information.
|
See the repository of the [main application](https://git.cantorgymnasium.de/cantortechnik/meincantor-app) for additional information.
|
||||||
|
|
||||||
@ -22,19 +22,15 @@ cargo build --release
|
|||||||
version: "3.1"
|
version: "3.1"
|
||||||
services:
|
services:
|
||||||
api:
|
api:
|
||||||
image: lxdb/meincantor-api
|
image: registry.cantorgymnasium.de/cantortechnik/meincantor-api
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- 8000:8000
|
- 8000:8000
|
||||||
environment:
|
environment:
|
||||||
IW_TIMETABLE_URL: https://stundenplan24.de/EXAMPLE_SCHOOL/mobil/mobdaten
|
IW_TIMETABLE: https://stundenplan24.de/EXAMPLE_SCHOOL/mobil/mobdaten
|
||||||
IW_TIMETABLE_USER: EXAMPLE_USER
|
IW_TIMETABLE_USER: EXAMPLE_USER
|
||||||
IW_TIMETABLE_PASSWORD: EXAMPLE_PASSWORD
|
IW_TIMETABLE_PASSWORD: EXAMPLE_PASSWORD
|
||||||
JWT_SECRET: EXAMPLE_SECRET
|
OIDC_USERINFO: https://keycloak.example.com/auth/realms/EXAMPLE_REALM/protocol/openid-connect/userinfo
|
||||||
JWT_ISSUER: Georg-Cantor-Gymnasium Halle(Saale)
|
|
||||||
KC_OPENID_TOKEN_ENDPOINT: https://example.keycloak.com/auth/realms/EXAMPLE_REALM/protocol/openid-connect/token
|
|
||||||
KC_OPENID_USERINFO_ENDPOINT: https://example.keycloak.com/auth/realms/EXAMPLE_REALM/protocol/openid-connect/userinfo
|
|
||||||
KC_CLIENT_ID: EXAMPLE_CLIENT
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./static:/app/static
|
- ./static:/app/static
|
||||||
```
|
```
|
||||||
|
@ -1,18 +1,14 @@
|
|||||||
version: "3.1"
|
version: "3.1"
|
||||||
services:
|
services:
|
||||||
api:
|
api:
|
||||||
image: lxdb/meincantor-api
|
image: registry.cantorgymnasium.de/cantortechnik/meincantor-api
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- 8000:8000
|
- 8000:8000
|
||||||
environment:
|
environment:
|
||||||
IW_TIMETABLE_URL: https://stundenplan24.de/EXAMPLE_SCHOOL/mobil/mobdaten
|
IW_TIMETABLE: https://stundenplan24.de/EXAMPLE_SCHOOL/mobil/mobdaten
|
||||||
IW_TIMETABLE_USER: EXAMPLE_USER
|
IW_TIMETABLE_USER: EXAMPLE_USER
|
||||||
IW_TIMETABLE_PASSWORD: EXAMPLE_PASSWORD
|
IW_TIMETABLE_PASSWORD: EXAMPLE_PASSWORD
|
||||||
JWT_SECRET: EXAMPLE_SECRET
|
OIDC_USERINFO: https://keycloak.example.com/auth/realms/EXAMPLE_REALM/protocol/openid-connect/userinfo
|
||||||
JWT_ISSUER: Georg-Cantor-Gymnasium Halle(Saale)
|
|
||||||
KC_OPENID_TOKEN_ENDPOINT: https://example.keycloak.com/auth/realms/EXAMPLE_REALM/protocol/openid-connect/token
|
|
||||||
KC_OPENID_USERINFO_ENDPOINT: https://example.keycloak.com/auth/realms/EXAMPLE_REALM/protocol/openid-connect/userinfo
|
|
||||||
KC_CLIENT_ID: EXAMPLE_CLIENT
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./static:/app/static
|
- ./static:/app/static
|
||||||
|
@ -15,17 +15,9 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// Timetable source
|
// Timetable source
|
||||||
pub static IW_TIMETABLE_URL: &str = "https://stundenplan24.de/EXAMPLE_SCHOOL/mobil/mobdaten";
|
pub static IW_TIMETABLE: &str = "https://stundenplan24.de/EXAMPLE_SCHOOL/mobil/mobdaten";
|
||||||
pub static IW_TIMETABLE_USER: &str = "EXAMPLE_USER";
|
pub static IW_TIMETABLE_USER: &str = "EXAMPLE_USER";
|
||||||
pub static IW_TIMETABLE_PASSWORD: &str = "EXAMPLE_PASSWORD";
|
pub static IW_TIMETABLE_PASSWORD: &str = "EXAMPLE_PASSWORD";
|
||||||
|
|
||||||
// JWT
|
pub static OIDC_USERINFO: &str =
|
||||||
pub static JWT_SECRET: &str = "EXAMPLE_SECRET";
|
|
||||||
pub static JWT_ISSUER: &str = "Georg-Cantor-Gymnasium Halle(Saale)";
|
|
||||||
|
|
||||||
// Keycloak
|
|
||||||
pub static KC_OPENID_TOKEN_ENDPOINT: &str =
|
|
||||||
"https://example.keycloak.com/auth/realms/EXAMPLE_REALM/protocol/openid-connect/token";
|
|
||||||
pub static KC_OPENID_USERINFO_ENDPOINT: &str =
|
|
||||||
"https://example.keycloak.com/auth/realms/EXAMPLE_REALM/protocol/openid-connect/userinfo";
|
"https://example.keycloak.com/auth/realms/EXAMPLE_REALM/protocol/openid-connect/userinfo";
|
||||||
pub static KC_CLIENT_ID: &str = "EXAMPLE_CLIENT";
|
|
||||||
|
@ -43,18 +43,17 @@ pub struct Lesson {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get_timetable_xml(url: &str) -> serde_json::value::Value {
|
async fn get_timetable_xml(url: &str) -> serde_json::value::Value {
|
||||||
let client = reqwest::Client::new();
|
let resp = reqwest::Client::new()
|
||||||
let resp = client
|
|
||||||
.get(format!(
|
.get(format!(
|
||||||
"{}/{}",
|
"{}/{}",
|
||||||
env::var("IW_TIMETABLE_URL").unwrap_or(config::IW_TIMETABLE_URL.to_string()),
|
env::var("IW_TIMETABLE").unwrap_or_else(|_| config::IW_TIMETABLE.to_string()),
|
||||||
url
|
url
|
||||||
))
|
))
|
||||||
.basic_auth(
|
.basic_auth(
|
||||||
env::var("IW_TIMETABLE_USER").unwrap_or(config::IW_TIMETABLE_USER.to_string()),
|
env::var("IW_TIMETABLE_USER").unwrap_or_else(|_| config::IW_TIMETABLE_USER.to_string()),
|
||||||
Some(
|
Some(
|
||||||
env::var("IW_TIMETABLE_PASSWORD")
|
env::var("IW_TIMETABLE_PASSWORD")
|
||||||
.unwrap_or(config::IW_TIMETABLE_PASSWORD.to_string()),
|
.unwrap_or_else(|_| config::IW_TIMETABLE_PASSWORD.to_string()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.send()
|
.send()
|
||||||
@ -67,8 +66,8 @@ async fn get_timetable_xml(url: &str) -> serde_json::value::Value {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get_timetable_xml_data(url: &str) -> Vec<serde_json::value::Value> {
|
async fn get_timetable_xml_data(url: &str) -> Vec<serde_json::value::Value> {
|
||||||
let xml = get_timetable_xml(url).await;
|
get_timetable_xml(url)
|
||||||
let classes = xml
|
.await
|
||||||
.as_object()
|
.as_object()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.get("VpMobil")
|
.get("VpMobil")
|
||||||
@ -78,14 +77,15 @@ async fn get_timetable_xml_data(url: &str) -> Vec<serde_json::value::Value> {
|
|||||||
.get("Kl")
|
.get("Kl")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_array()
|
.as_array()
|
||||||
.unwrap();
|
.unwrap()
|
||||||
classes.to_owned()
|
.to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_timetable(url: String) -> Vec<Timetable> {
|
pub async fn get_timetable(url: String) -> Vec<Timetable> {
|
||||||
let xml = get_timetable_xml(&url).await;
|
let xml = get_timetable_xml(&url).await;
|
||||||
let classes = get_timetable_xml_data(&url).await;
|
let classes = get_timetable_xml_data(&url).await;
|
||||||
let mut timetable: Vec<Timetable> = Vec::new();
|
let mut timetable: Vec<Timetable> = Vec::new();
|
||||||
|
//dbg!(&classes);
|
||||||
for i in classes.iter() {
|
for i in classes.iter() {
|
||||||
let mut courses: Vec<rocket::serde::json::Value> = Vec::new();
|
let mut courses: Vec<rocket::serde::json::Value> = Vec::new();
|
||||||
let nothing = json!([""]);
|
let nothing = json!([""]);
|
||||||
@ -104,11 +104,11 @@ pub async fn get_timetable(url: String) -> Vec<Timetable> {
|
|||||||
} else if std.is_object() {
|
} else if std.is_object() {
|
||||||
plan.push(std.clone())
|
plan.push(std.clone())
|
||||||
}
|
}
|
||||||
for i in &plan {
|
for x in &plan {
|
||||||
if i.as_object() != None {
|
if x.as_object() != None {
|
||||||
courses.push(i.to_owned());
|
courses.push(x.to_owned());
|
||||||
} else {
|
} else {
|
||||||
dbg!("Failed: {:?}", &i);
|
dbg!("Failed to decode plan: {:?}", &i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let empty_list = serde_json::Value::Array(vec![]);
|
let empty_list = serde_json::Value::Array(vec![]);
|
||||||
@ -132,36 +132,16 @@ pub async fn get_timetable(url: String) -> Vec<Timetable> {
|
|||||||
} else if info_value.is_string() {
|
} else if info_value.is_string() {
|
||||||
info.push_str(info_value.as_str().unwrap_or(""));
|
info.push_str(info_value.as_str().unwrap_or(""));
|
||||||
}
|
}
|
||||||
let response = TimetableData {
|
let header = xml
|
||||||
count: plan.len(),
|
.as_object()
|
||||||
courses,
|
.unwrap()
|
||||||
info,
|
.get("VpMobil")
|
||||||
};
|
.unwrap()
|
||||||
|
.get("Kopf")
|
||||||
|
.unwrap();
|
||||||
let timetable_element = Timetable {
|
let timetable_element = Timetable {
|
||||||
date: String::from(
|
date: String::from(header.get("DatumPlan").unwrap().as_str().unwrap()),
|
||||||
xml.as_object()
|
updated: String::from(header.get("zeitstempel").unwrap().as_str().unwrap()),
|
||||||
.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(
|
class: String::from(
|
||||||
i.as_object()
|
i.as_object()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@ -170,167 +150,87 @@ pub async fn get_timetable(url: String) -> Vec<Timetable> {
|
|||||||
.as_str()
|
.as_str()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
),
|
),
|
||||||
timetable_data: json!(response),
|
timetable_data: json!(TimetableData {
|
||||||
|
count: plan.len(),
|
||||||
|
courses,
|
||||||
|
info,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
timetable.push(timetable_element)
|
timetable.push(timetable_element)
|
||||||
}
|
}
|
||||||
let normal_classes: Vec<Timetable> = timetable
|
let normal_classes: Vec<Timetable> = timetable
|
||||||
.to_vec()
|
.iter()
|
||||||
.into_iter()
|
.cloned()
|
||||||
.filter(|e| !e.class.contains("11") && !e.class.contains("12"))
|
.filter(|e| !e.class.contains("11") && !e.class.contains("12"))
|
||||||
.collect();
|
.collect();
|
||||||
let eleven_classes: Vec<Timetable> = timetable
|
|
||||||
.to_vec()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|e| e.class.contains("11"))
|
|
||||||
.collect();
|
|
||||||
let twelve_classes: Vec<Timetable> = timetable
|
|
||||||
.to_vec()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|e| e.class.contains("12"))
|
|
||||||
.collect();
|
|
||||||
let eleven_timetable_data = {
|
|
||||||
let mut courses: Vec<serde_json::Value> = Vec::new();
|
|
||||||
let mut info: String = String::new();
|
|
||||||
for i in eleven_classes {
|
|
||||||
courses.extend(
|
|
||||||
i.timetable_data
|
|
||||||
.as_object()
|
|
||||||
.unwrap()
|
|
||||||
.get("courses")
|
|
||||||
.unwrap()
|
|
||||||
.as_array()
|
|
||||||
.unwrap()
|
|
||||||
.to_owned(),
|
|
||||||
);
|
|
||||||
info = i
|
|
||||||
.timetable_data
|
|
||||||
.as_object()
|
|
||||||
.unwrap()
|
|
||||||
.get("info")
|
|
||||||
.unwrap()
|
|
||||||
.as_str()
|
|
||||||
.unwrap()
|
|
||||||
.to_owned();
|
|
||||||
}
|
|
||||||
courses.sort_by(|a, b| {
|
|
||||||
let n1 = a.as_object().unwrap().get("St").unwrap().as_i64().unwrap();
|
|
||||||
let sb1 = a.as_object().unwrap().get("Fa").unwrap().as_str().unwrap();
|
|
||||||
let n2 = b.as_object().unwrap().get("St").unwrap().as_i64().unwrap();
|
|
||||||
let sb2 = b.as_object().unwrap().get("Fa").unwrap().as_str().unwrap();
|
|
||||||
if n1 == n2 {
|
|
||||||
sb1.cmp(&sb2)
|
|
||||||
} else {
|
|
||||||
n1.cmp(&n2)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
TimetableData {
|
|
||||||
count: courses.len(),
|
|
||||||
courses,
|
|
||||||
info,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let eleven = 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("11"),
|
|
||||||
timetable_data: json!(eleven_timetable_data),
|
|
||||||
};
|
|
||||||
let twelve_timetable_data = {
|
|
||||||
let mut courses: Vec<serde_json::Value> = Vec::new();
|
|
||||||
let mut info: String = String::new();
|
|
||||||
for i in twelve_classes {
|
|
||||||
courses.extend(
|
|
||||||
i.timetable_data
|
|
||||||
.as_object()
|
|
||||||
.unwrap()
|
|
||||||
.get("courses")
|
|
||||||
.unwrap()
|
|
||||||
.as_array()
|
|
||||||
.unwrap()
|
|
||||||
.to_owned(),
|
|
||||||
);
|
|
||||||
info = i
|
|
||||||
.timetable_data
|
|
||||||
.as_object()
|
|
||||||
.unwrap()
|
|
||||||
.get("info")
|
|
||||||
.unwrap()
|
|
||||||
.as_str()
|
|
||||||
.unwrap()
|
|
||||||
.to_owned();
|
|
||||||
}
|
|
||||||
courses.sort_by(|a, b| {
|
|
||||||
let n1 = a.as_object().unwrap().get("St").unwrap().as_i64().unwrap();
|
|
||||||
let sb1 = a.as_object().unwrap().get("Fa").unwrap().as_str().unwrap();
|
|
||||||
let n2 = b.as_object().unwrap().get("St").unwrap().as_i64().unwrap();
|
|
||||||
let sb2 = b.as_object().unwrap().get("Fa").unwrap().as_str().unwrap();
|
|
||||||
if n1 == n2 {
|
|
||||||
sb1.cmp(&sb2)
|
|
||||||
} else {
|
|
||||||
n1.cmp(&n2)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
TimetableData {
|
|
||||||
count: courses.len(),
|
|
||||||
courses,
|
|
||||||
info,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let twelve = 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("12"),
|
|
||||||
timetable_data: json!(twelve_timetable_data),
|
|
||||||
};
|
|
||||||
let mut timetable_refactored: Vec<Timetable> = Vec::new();
|
let mut timetable_refactored: Vec<Timetable> = Vec::new();
|
||||||
timetable_refactored.extend(normal_classes);
|
timetable_refactored.extend(normal_classes);
|
||||||
timetable_refactored.push(eleven);
|
for year in ["11", "12"] {
|
||||||
timetable_refactored.push(twelve);
|
let class: Vec<Timetable> = timetable
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.filter(|e| e.class.contains(year))
|
||||||
|
.collect();
|
||||||
|
let timetable_data = {
|
||||||
|
let mut courses: Vec<serde_json::Value> = Vec::new();
|
||||||
|
let mut info: String = String::new();
|
||||||
|
for i in class {
|
||||||
|
let td_obj = i.timetable_data.as_object().unwrap();
|
||||||
|
courses.extend(
|
||||||
|
td_obj
|
||||||
|
.get("courses")
|
||||||
|
.unwrap()
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.to_owned(),
|
||||||
|
);
|
||||||
|
info = td_obj.get("info").unwrap().as_str().unwrap().to_owned();
|
||||||
|
}
|
||||||
|
courses.sort_by(|a, b| {
|
||||||
|
fn st_nr(obj: &serde_json::Value) -> i64 {
|
||||||
|
obj.as_object()
|
||||||
|
.unwrap()
|
||||||
|
.get("St")
|
||||||
|
.unwrap_or(&json!([""]))
|
||||||
|
.as_i64()
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
fn fa_abc(obj: &serde_json::Value) -> String {
|
||||||
|
obj.as_object()
|
||||||
|
.unwrap()
|
||||||
|
.get("Fa")
|
||||||
|
.unwrap_or(&json!([""]))
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_owned()
|
||||||
|
}
|
||||||
|
if st_nr(a) == st_nr(b) {
|
||||||
|
fa_abc(a).cmp(&fa_abc(b))
|
||||||
|
} else {
|
||||||
|
st_nr(a).cmp(&st_nr(b))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
TimetableData {
|
||||||
|
count: courses.len(),
|
||||||
|
courses,
|
||||||
|
info,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let header = xml
|
||||||
|
.as_object()
|
||||||
|
.unwrap()
|
||||||
|
.get("VpMobil")
|
||||||
|
.unwrap()
|
||||||
|
.get("Kopf")
|
||||||
|
.unwrap();
|
||||||
|
let timetable = Timetable {
|
||||||
|
date: String::from(header.get("DatumPlan").unwrap().as_str().unwrap()),
|
||||||
|
updated: String::from(header.get("zeitstempel").unwrap().as_str().unwrap()),
|
||||||
|
class: year.to_string(),
|
||||||
|
timetable_data: json!(timetable_data),
|
||||||
|
};
|
||||||
|
timetable_refactored.push(timetable);
|
||||||
|
}
|
||||||
timetable_refactored
|
timetable_refactored
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,15 +265,16 @@ pub async fn get_class_timetable(class: String, url: String) -> TimetableData {
|
|||||||
info,
|
info,
|
||||||
};
|
};
|
||||||
for i in classes.iter() {
|
for i in classes.iter() {
|
||||||
if i.as_object()
|
let current_class = i
|
||||||
|
.as_object()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.get("Kurz")
|
.get("Kurz")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_str()
|
.as_str()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.replace("/", "_")
|
.replace('/', "_");
|
||||||
== class
|
let contains_class = current_class.contains(&class);
|
||||||
{
|
if class == current_class {
|
||||||
let nothing = json!([""]);
|
let nothing = json!([""]);
|
||||||
let std = i
|
let std = i
|
||||||
.as_object()
|
.as_object()
|
||||||
@ -399,16 +300,7 @@ pub async fn get_class_timetable(class: String, url: String) -> TimetableData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
} else if class == String::from("11")
|
} else if (class == *"11" || class == *"12") && contains_class {
|
||||||
&& i.as_object()
|
|
||||||
.unwrap()
|
|
||||||
.get("Kurz")
|
|
||||||
.unwrap()
|
|
||||||
.as_str()
|
|
||||||
.unwrap()
|
|
||||||
.replace("/", "_")
|
|
||||||
.contains(&class)
|
|
||||||
{
|
|
||||||
let nothing = json!([""]);
|
let nothing = json!([""]);
|
||||||
let std = i
|
let std = i
|
||||||
.as_object()
|
.as_object()
|
||||||
@ -433,48 +325,28 @@ pub async fn get_class_timetable(class: String, url: String) -> TimetableData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
response.courses.sort_by(|a, b| {
|
response.courses.sort_by(|a, b| {
|
||||||
let n1 = a.as_object().unwrap().get("St").unwrap().as_i64().unwrap();
|
fn st_nr(obj: &serde_json::Value) -> i64 {
|
||||||
let n2 = b.as_object().unwrap().get("St").unwrap().as_i64().unwrap();
|
obj.as_object()
|
||||||
n1.cmp(&n2)
|
.unwrap()
|
||||||
});
|
.get("St")
|
||||||
response.count = response.courses.len();
|
.unwrap_or(&json!([""]))
|
||||||
} else if class == String::from("12")
|
.as_i64()
|
||||||
&& i.as_object()
|
.unwrap_or_default()
|
||||||
.unwrap()
|
}
|
||||||
.get("Kurz")
|
fn fa_abc(obj: &serde_json::Value) -> String {
|
||||||
.unwrap()
|
obj.as_object()
|
||||||
.as_str()
|
.unwrap()
|
||||||
.unwrap()
|
.get("Fa")
|
||||||
.replace("/", "_")
|
.unwrap_or(&json!([""]))
|
||||||
.contains(&class)
|
.as_str()
|
||||||
{
|
.unwrap_or_default()
|
||||||
let nothing = json!([""]);
|
.to_owned()
|
||||||
let std = i
|
}
|
||||||
.as_object()
|
if st_nr(a) == st_nr(b) {
|
||||||
.unwrap()
|
fa_abc(a).cmp(&fa_abc(b))
|
||||||
.get("Pl")
|
} else {
|
||||||
.unwrap()
|
st_nr(a).cmp(&st_nr(b))
|
||||||
.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 {
|
|
||||||
response.courses.push(i.to_owned());
|
|
||||||
} else {
|
|
||||||
dbg!("Failed: {:?}", &i);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
response.courses.sort_by(|a, b| {
|
|
||||||
let n1 = a.as_object().unwrap().get("St").unwrap().as_i64().unwrap();
|
|
||||||
let n2 = b.as_object().unwrap().get("St").unwrap().as_i64().unwrap();
|
|
||||||
n1.cmp(&n2)
|
|
||||||
});
|
});
|
||||||
response.count = response.courses.len();
|
response.count = response.courses.len();
|
||||||
}
|
}
|
||||||
@ -486,41 +358,20 @@ pub async fn get_classes() -> Vec<String> {
|
|||||||
let classes = get_timetable_xml_data(&String::from("Klassen.xml")).await;
|
let classes = get_timetable_xml_data(&String::from("Klassen.xml")).await;
|
||||||
let mut class_list: Vec<String> = Vec::new();
|
let mut class_list: Vec<String> = Vec::new();
|
||||||
for i in classes.iter() {
|
for i in classes.iter() {
|
||||||
if String::from(
|
let current_class = i
|
||||||
i.as_object()
|
.as_object()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.get("Kurz")
|
.get("Kurz")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_str()
|
.as_str()
|
||||||
.unwrap(),
|
.unwrap()
|
||||||
)
|
.to_string();
|
||||||
.contains("11")
|
if current_class.contains("11") && !class_list.contains(&"11".to_string()) {
|
||||||
{
|
class_list.push("11".to_string());
|
||||||
if !class_list.contains(&"11".to_string()) {
|
} else if current_class.contains("12") && !class_list.contains(&"12".to_string()) {
|
||||||
class_list.push("11".to_string());
|
class_list.push("12".to_string());
|
||||||
}
|
} else if !current_class.contains("11") && !current_class.contains("12") {
|
||||||
} else if String::from(
|
class_list.push(current_class);
|
||||||
i.as_object()
|
|
||||||
.unwrap()
|
|
||||||
.get("Kurz")
|
|
||||||
.unwrap()
|
|
||||||
.as_str()
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.contains("12")
|
|
||||||
{
|
|
||||||
if !class_list.contains(&"12".to_string()) {
|
|
||||||
class_list.push("12".to_string());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
class_list.push(String::from(
|
|
||||||
i.as_object()
|
|
||||||
.unwrap()
|
|
||||||
.get("Kurz")
|
|
||||||
.unwrap()
|
|
||||||
.as_str()
|
|
||||||
.unwrap(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class_list
|
class_list
|
||||||
@ -531,85 +382,16 @@ pub async fn get_class_lessons(class: String) -> Vec<Lesson> {
|
|||||||
let mut lesson_list: Vec<Lesson> = Vec::new();
|
let mut lesson_list: Vec<Lesson> = Vec::new();
|
||||||
for i in classes.iter() {
|
for i in classes.iter() {
|
||||||
let empty_list = serde_json::Value::Array(Vec::new());
|
let empty_list = serde_json::Value::Array(Vec::new());
|
||||||
if i.as_object()
|
let current_class = i
|
||||||
|
.as_object()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.get("Kurz")
|
.get("Kurz")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_str()
|
.as_str()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.replace("/", "_")
|
.replace('/', "_");
|
||||||
== class
|
let contains_class = current_class.contains(&class);
|
||||||
{
|
if (class == current_class) || ((class == *"11" || class == *"12") && contains_class) {
|
||||||
let class_lessons = i
|
|
||||||
.as_object()
|
|
||||||
.unwrap()
|
|
||||||
.get("Unterricht")
|
|
||||||
.unwrap()
|
|
||||||
.as_object()
|
|
||||||
.unwrap()
|
|
||||||
.get("Ue")
|
|
||||||
.unwrap_or(&empty_list)
|
|
||||||
.as_array()
|
|
||||||
.unwrap();
|
|
||||||
for lesson in class_lessons.iter() {
|
|
||||||
let lesson = lesson
|
|
||||||
.as_object()
|
|
||||||
.unwrap()
|
|
||||||
.get("UeNr")
|
|
||||||
.unwrap()
|
|
||||||
.as_object()
|
|
||||||
.unwrap();
|
|
||||||
lesson_list.push(Lesson {
|
|
||||||
subject: lesson.get("@UeFa").unwrap().as_str().unwrap().to_string(),
|
|
||||||
teacher: lesson.get("@UeLe").unwrap().as_str().unwrap().to_string(),
|
|
||||||
id: lesson.get("#text").unwrap().as_i64().unwrap(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else if class == String::from("11")
|
|
||||||
&& i.as_object()
|
|
||||||
.unwrap()
|
|
||||||
.get("Kurz")
|
|
||||||
.unwrap()
|
|
||||||
.as_str()
|
|
||||||
.unwrap()
|
|
||||||
.replace("/", "_")
|
|
||||||
.contains(&class)
|
|
||||||
{
|
|
||||||
let class_lessons = i
|
|
||||||
.as_object()
|
|
||||||
.unwrap()
|
|
||||||
.get("Unterricht")
|
|
||||||
.unwrap()
|
|
||||||
.as_object()
|
|
||||||
.unwrap()
|
|
||||||
.get("Ue")
|
|
||||||
.unwrap_or(&empty_list)
|
|
||||||
.as_array()
|
|
||||||
.unwrap();
|
|
||||||
for lesson in class_lessons.iter() {
|
|
||||||
let lesson = lesson
|
|
||||||
.as_object()
|
|
||||||
.unwrap()
|
|
||||||
.get("UeNr")
|
|
||||||
.unwrap()
|
|
||||||
.as_object()
|
|
||||||
.unwrap();
|
|
||||||
lesson_list.push(Lesson {
|
|
||||||
subject: lesson.get("@UeFa").unwrap().as_str().unwrap().to_string(),
|
|
||||||
teacher: lesson.get("@UeLe").unwrap().as_str().unwrap().to_string(),
|
|
||||||
id: lesson.get("#text").unwrap().as_i64().unwrap(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else if class == String::from("12")
|
|
||||||
&& i.as_object()
|
|
||||||
.unwrap()
|
|
||||||
.get("Kurz")
|
|
||||||
.unwrap()
|
|
||||||
.as_str()
|
|
||||||
.unwrap()
|
|
||||||
.replace("/", "_")
|
|
||||||
.contains(&class)
|
|
||||||
{
|
|
||||||
let class_lessons = i
|
let class_lessons = i
|
||||||
.as_object()
|
.as_object()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -1,282 +0,0 @@
|
|||||||
// GCG.MeinCantor.API - The server-part of GCG.MeinCantor - The school application for the Georg-Cantor-Gymnasium
|
|
||||||
// Copyright (C) 2021-2022 Denys Konovalov
|
|
||||||
|
|
||||||
// This program is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Affero General Public License as published
|
|
||||||
// by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// any later version.
|
|
||||||
|
|
||||||
// This program is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
extern crate reqwest;
|
|
||||||
|
|
||||||
use crate::config;
|
|
||||||
use crate::{Claims, Credentials, Roles, Token, TokenStatus};
|
|
||||||
|
|
||||||
use jsonwebtoken::{encode, EncodingKey, Header};
|
|
||||||
use rocket::{response::status, serde::json::Json};
|
|
||||||
use serde_derive::{Deserialize, Serialize};
|
|
||||||
use serde_json::json;
|
|
||||||
use std::env;
|
|
||||||
use std::error::Error;
|
|
||||||
use std::fmt::Display;
|
|
||||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
||||||
use time::{macros::format_description, 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,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct KeycloakHttpError {
|
|
||||||
pub error: Option<String>,
|
|
||||||
#[serde(rename = "errorMessage")]
|
|
||||||
pub error_message: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum KeycloakError {
|
|
||||||
ReqwestFailure(reqwest::Error),
|
|
||||||
HttpFailure {
|
|
||||||
status: u16,
|
|
||||||
body: Option<KeycloakHttpError>,
|
|
||||||
text: String,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<reqwest::Error> for KeycloakError {
|
|
||||||
fn from(value: reqwest::Error) -> Self {
|
|
||||||
KeycloakError::ReqwestFailure(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Error for KeycloakError {
|
|
||||||
fn description(&self) -> &str {
|
|
||||||
"keycloak error"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cause(&self) -> Option<&dyn Error> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for KeycloakError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "keycloak error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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",
|
|
||||||
env::var("KC_CLIENT_ID").unwrap_or(config::KC_CLIENT_ID.to_string()),
|
|
||||||
),
|
|
||||||
("grant_type", String::from("password")),
|
|
||||||
];
|
|
||||||
let resp = client
|
|
||||||
.post(
|
|
||||||
env::var("KC_OPENID_TOKEN_ENDPOINT")
|
|
||||||
.unwrap_or(config::KC_OPENID_TOKEN_ENDPOINT.to_string()),
|
|
||||||
)
|
|
||||||
.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(
|
|
||||||
env::var("KC_OPENID_USERINFO_ENDPOINT")
|
|
||||||
.unwrap_or(config::KC_OPENID_USERINFO_ENDPOINT.to_string()),
|
|
||||||
)
|
|
||||||
.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(format_description!(
|
|
||||||
"[day]/[month]/[year] [hour]:[minute]:[second]"
|
|
||||||
));
|
|
||||||
let my_claims = Claims {
|
|
||||||
iss: env::var("JWT_ISSUER").unwrap_or(config::JWT_ISSUER.to_string()),
|
|
||||||
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.unwrap()),
|
|
||||||
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(
|
|
||||||
env::var("JWT_SECRET")
|
|
||||||
.unwrap_or(config::JWT_SECRET.to_string())
|
|
||||||
.as_ref(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
Ok(Json(Token {
|
|
||||||
outcome: (TokenStatus::Success, String::new()),
|
|
||||||
token: jwt.unwrap(),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
_ => Err(status::Unauthorized::<String>(Some(token.outcome.1))),
|
|
||||||
};
|
|
||||||
result
|
|
||||||
}
|
|
192
src/main.rs
192
src/main.rs
@ -19,24 +19,74 @@ extern crate rocket;
|
|||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
mod indiware_connector;
|
mod indiware_connector;
|
||||||
mod keycloak_connector;
|
|
||||||
|
|
||||||
extern crate reqwest;
|
extern crate reqwest;
|
||||||
extern crate serde;
|
extern crate serde;
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
|
|
||||||
use indiware_connector as timetable_connector;
|
use indiware_connector as timetable_connector;
|
||||||
use jsonwebtoken::{decode, DecodingKey, Validation};
|
|
||||||
use keycloak_connector::KeycloakUser;
|
|
||||||
use rocket::{
|
use rocket::{
|
||||||
fs::{relative, FileServer},
|
fs::{relative, FileServer},
|
||||||
http::Status,
|
http::Status,
|
||||||
request::{FromRequest, Outcome, Request},
|
request::{FromRequest, Outcome, Request},
|
||||||
response::status,
|
|
||||||
serde::json::Json,
|
serde::json::Json,
|
||||||
};
|
};
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
use std::env;
|
use std::{env, error::Error, fmt::Display};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct OpenidConnectUser {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct OpenidConnectHttpError {
|
||||||
|
pub error: Option<String>,
|
||||||
|
#[serde(rename = "errorMessage")]
|
||||||
|
pub error_message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum OpenidConnectError {
|
||||||
|
ReqwestFailure(reqwest::Error),
|
||||||
|
HttpFailure {
|
||||||
|
status: u16,
|
||||||
|
body: Option<OpenidConnectHttpError>,
|
||||||
|
text: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<reqwest::Error> for OpenidConnectError {
|
||||||
|
fn from(value: reqwest::Error) -> Self {
|
||||||
|
OpenidConnectError::ReqwestFailure(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for OpenidConnectError {
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"keycloak error"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cause(&self) -> Option<&dyn Error> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for OpenidConnectError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "keycloak error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct Credentials {
|
pub struct Credentials {
|
||||||
@ -81,35 +131,43 @@ pub struct Token {
|
|||||||
struct ApiKey<'r>(&'r str);
|
struct ApiKey<'r>(&'r str);
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum ApiKeyError {
|
enum TokenError {
|
||||||
Missing,
|
Missing,
|
||||||
Invalid,
|
Invalid,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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 = TokenError;
|
||||||
|
|
||||||
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.
|
async fn request_valid(token: &str) -> Result<reqwest::Response, reqwest::Error> {
|
||||||
fn is_valid(key: &str) -> bool {
|
let client = reqwest::Client::new();
|
||||||
let mut validation = Validation::default();
|
client
|
||||||
validation.validate_exp = false;
|
.get(
|
||||||
let token = decode::<Claims>(
|
env::var("OIDC_USERINFO").unwrap_or_else(|_| config::OIDC_USERINFO.to_string()),
|
||||||
key,
|
)
|
||||||
&DecodingKey::from_secret(
|
.header("Authorization", format!("Bearer {}", token))
|
||||||
env::var("JWT_SECRET")
|
.send()
|
||||||
.unwrap_or(config::JWT_SECRET.to_string())
|
.await
|
||||||
.as_ref(),
|
|
||||||
),
|
|
||||||
&validation,
|
|
||||||
);
|
|
||||||
token.is_ok()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_permissions(key: &str, uri: &str) -> bool {
|
async fn token_valid(
|
||||||
let mut validation = Validation::default();
|
resp: reqwest::Response,
|
||||||
validation.validate_exp = false;
|
) -> Result<reqwest::Response, OpenidConnectError> {
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
let status = resp.status().into();
|
||||||
|
let text = resp.text().await?;
|
||||||
|
return Err(OpenidConnectError::HttpFailure {
|
||||||
|
status,
|
||||||
|
body: serde_json::from_str(&text).ok(),
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn has_permissions(kc_user: reqwest::Response, uri: &str) -> bool {
|
||||||
|
let user = kc_user.json::<OpenidConnectUser>().await.unwrap();
|
||||||
let standard_permissions = vec![
|
let standard_permissions = vec![
|
||||||
String::from("/api/timetable"),
|
String::from("/api/timetable"),
|
||||||
String::from("/api/classes"),
|
String::from("/api/classes"),
|
||||||
@ -117,19 +175,9 @@ impl<'r> FromRequest<'r> for ApiKey<'r> {
|
|||||||
];
|
];
|
||||||
let student_permissions: Vec<String> = vec![];
|
let student_permissions: Vec<String> = vec![];
|
||||||
let teacher_permissions: Vec<String> = vec![];
|
let teacher_permissions: Vec<String> = vec![];
|
||||||
let token = decode::<Claims>(
|
|
||||||
key,
|
|
||||||
&DecodingKey::from_secret(
|
|
||||||
env::var("JWT_SECRET")
|
|
||||||
.unwrap_or(config::JWT_SECRET.to_string())
|
|
||||||
.as_ref(),
|
|
||||||
),
|
|
||||||
&validation,
|
|
||||||
);
|
|
||||||
let token = token.unwrap();
|
|
||||||
let mut permissions = Vec::new();
|
let mut permissions = Vec::new();
|
||||||
permissions.extend(standard_permissions.iter().cloned());
|
permissions.extend(standard_permissions.iter().cloned());
|
||||||
for role in token.claims.roles.iter() {
|
for role in user.roles.iter() {
|
||||||
match role {
|
match role {
|
||||||
Roles::Admin => permissions.push(String::from("all")),
|
Roles::Admin => permissions.push(String::from("all")),
|
||||||
Roles::Student => permissions.extend(student_permissions.iter().cloned()),
|
Roles::Student => permissions.extend(student_permissions.iter().cloned()),
|
||||||
@ -138,43 +186,55 @@ impl<'r> FromRequest<'r> for ApiKey<'r> {
|
|||||||
}
|
}
|
||||||
permissions.contains(&String::from(uri))
|
permissions.contains(&String::from(uri))
|
||||||
| permissions.contains(&String::from("all"))
|
| permissions.contains(&String::from("all"))
|
||||||
| token.claims.whitelist.contains(&String::from(uri))
|
| user
|
||||||
&& !token.claims.blacklist.contains(&String::from(uri))
|
.whitelist
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains(&String::from(uri))
|
||||||
|
&& !user
|
||||||
|
.blacklist
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains(&String::from(uri))
|
||||||
}
|
}
|
||||||
|
|
||||||
match req.headers().get_one("x-api-key") {
|
match req.headers().get_one("x-api-key") {
|
||||||
None => Outcome::Failure((Status::Unauthorized, ApiKeyError::Missing)),
|
None => Outcome::Failure((Status::Unauthorized, TokenError::Missing)),
|
||||||
Some(key)
|
Some(token) => {
|
||||||
if is_valid(key)
|
match request_valid(token).await {
|
||||||
&& has_permissions(key, req.route().unwrap().uri.base.path().as_str()) =>
|
Ok(resp) => match token_valid(resp).await {
|
||||||
{
|
Ok(kc_user) => {
|
||||||
Outcome::Success(ApiKey(key))
|
if has_permissions(
|
||||||
|
kc_user,
|
||||||
|
req.route().unwrap().uri.base.path().as_str(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Outcome::Success(ApiKey(token))
|
||||||
|
} else {
|
||||||
|
Outcome::Failure((Status::Unauthorized, TokenError::Invalid))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(kc_err) => {
|
||||||
|
match kc_err {
|
||||||
|
OpenidConnectError::ReqwestFailure(e) => Outcome::Failure((
|
||||||
|
Status::new(e.status().unwrap().as_u16()),
|
||||||
|
TokenError::Invalid,
|
||||||
|
)),
|
||||||
|
OpenidConnectError::HttpFailure {
|
||||||
|
status,
|
||||||
|
body: _,
|
||||||
|
text: _,
|
||||||
|
} => Outcome::Failure((Status::new(status), TokenError::Invalid)),
|
||||||
|
}
|
||||||
|
//
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => Outcome::Failure((Status::BadRequest, TokenError::Invalid)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Some(key)
|
|
||||||
if is_valid(key)
|
|
||||||
&& !has_permissions(key, req.route().unwrap().uri.base.path().as_str()) =>
|
|
||||||
{
|
|
||||||
Outcome::Failure((Status::Unauthorized, ApiKeyError::Invalid))
|
|
||||||
}
|
|
||||||
Some(_) => Outcome::Failure((Status::BadRequest, ApiKeyError::Invalid)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/", data = "<credentials>")]
|
|
||||||
async fn get_userinfo(
|
|
||||||
credentials: Json<Credentials>,
|
|
||||||
) -> Result<Json<KeycloakUser>, status::Unauthorized<String>> {
|
|
||||||
keycloak_connector::get_userinfo(credentials).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/", data = "<credentials>")]
|
|
||||||
async fn login(
|
|
||||||
credentials: Json<Credentials>,
|
|
||||||
) -> Result<Json<Token>, status::Unauthorized<String>> {
|
|
||||||
keycloak_connector::login(credentials).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/latest")]
|
#[get("/latest")]
|
||||||
async fn get_latest_timetable(_key: ApiKey<'_>) -> Json<Vec<timetable_connector::Timetable>> {
|
async fn get_latest_timetable(_key: ApiKey<'_>) -> Json<Vec<timetable_connector::Timetable>> {
|
||||||
let timetable = timetable_connector::get_timetable(String::from("Klassen.xml")).await;
|
let timetable = timetable_connector::get_timetable(String::from("Klassen.xml")).await;
|
||||||
@ -230,7 +290,6 @@ async fn get_class_lessons(
|
|||||||
fn rocket() -> _ {
|
fn rocket() -> _ {
|
||||||
rocket::build()
|
rocket::build()
|
||||||
.mount("/", FileServer::from(relative!("static")))
|
.mount("/", FileServer::from(relative!("static")))
|
||||||
.mount("/login", routes![login])
|
|
||||||
.mount(
|
.mount(
|
||||||
"/api/timetable",
|
"/api/timetable",
|
||||||
routes![
|
routes![
|
||||||
@ -242,5 +301,4 @@ fn rocket() -> _ {
|
|||||||
)
|
)
|
||||||
.mount("/api/classes", routes![get_classes])
|
.mount("/api/classes", routes![get_classes])
|
||||||
.mount("/api/lessons", routes![get_class_lessons])
|
.mount("/api/lessons", routes![get_class_lessons])
|
||||||
.mount("/api/userinfo", routes![get_userinfo])
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user