Change authentication/authorization flow, code cleanup, severa fixes
This commit is contained in:
parent
5db7f250e3
commit
31486a9d5e
@ -1,19 +1,16 @@
|
||||
[package]
|
||||
name = "api"
|
||||
version = "1.0.0"
|
||||
version = "2.0.0-alpha.1"
|
||||
edition = "2018"
|
||||
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
|
||||
|
||||
[dependencies]
|
||||
rocket = { version = "0.5.0-rc.1", features = ["json"] }
|
||||
rocket = { version = "0.5.0-rc.2", features = ["json"] }
|
||||
serde = "1.0"
|
||||
reqwest = { version="0.11", features = ["json"] }
|
||||
quickxml_to_serde = "0.5"
|
||||
serde_json = "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
|
||||
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.
|
||||
|
||||
@ -22,19 +22,15 @@ cargo build --release
|
||||
version: "3.1"
|
||||
services:
|
||||
api:
|
||||
image: lxdb/meincantor-api
|
||||
image: registry.cantorgymnasium.de/cantortechnik/meincantor-api
|
||||
restart: always
|
||||
ports:
|
||||
- 8000:8000
|
||||
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_PASSWORD: EXAMPLE_PASSWORD
|
||||
JWT_SECRET: EXAMPLE_SECRET
|
||||
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
|
||||
OIDC_USERINFO: https://keycloak.example.com/auth/realms/EXAMPLE_REALM/protocol/openid-connect/userinfo
|
||||
volumes:
|
||||
- ./static:/app/static
|
||||
```
|
||||
|
@ -1,18 +1,14 @@
|
||||
version: "3.1"
|
||||
services:
|
||||
api:
|
||||
image: lxdb/meincantor-api
|
||||
image: registry.cantorgymnasium.de/cantortechnik/meincantor-api
|
||||
restart: always
|
||||
ports:
|
||||
- 8000:8000
|
||||
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_PASSWORD: EXAMPLE_PASSWORD
|
||||
JWT_SECRET: EXAMPLE_SECRET
|
||||
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
|
||||
OIDC_USERINFO: https://keycloak.example.com/auth/realms/EXAMPLE_REALM/protocol/openid-connect/userinfo
|
||||
volumes:
|
||||
- ./static:/app/static
|
||||
|
@ -15,17 +15,9 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// 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_PASSWORD: &str = "EXAMPLE_PASSWORD";
|
||||
|
||||
// JWT
|
||||
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 =
|
||||
pub static OIDC_USERINFO: &str =
|
||||
"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 {
|
||||
let client = reqwest::Client::new();
|
||||
let resp = client
|
||||
let resp = reqwest::Client::new()
|
||||
.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
|
||||
))
|
||||
.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(
|
||||
env::var("IW_TIMETABLE_PASSWORD")
|
||||
.unwrap_or(config::IW_TIMETABLE_PASSWORD.to_string()),
|
||||
.unwrap_or_else(|_| config::IW_TIMETABLE_PASSWORD.to_string()),
|
||||
),
|
||||
)
|
||||
.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> {
|
||||
let xml = get_timetable_xml(url).await;
|
||||
let classes = xml
|
||||
get_timetable_xml(url)
|
||||
.await
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.get("VpMobil")
|
||||
@ -78,14 +77,15 @@ async fn get_timetable_xml_data(url: &str) -> Vec<serde_json::value::Value> {
|
||||
.get("Kl")
|
||||
.unwrap()
|
||||
.as_array()
|
||||
.unwrap();
|
||||
classes.to_owned()
|
||||
.unwrap()
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
pub async fn get_timetable(url: String) -> Vec<Timetable> {
|
||||
let xml = get_timetable_xml(&url).await;
|
||||
let classes = get_timetable_xml_data(&url).await;
|
||||
let mut timetable: Vec<Timetable> = Vec::new();
|
||||
//dbg!(&classes);
|
||||
for i in classes.iter() {
|
||||
let mut courses: Vec<rocket::serde::json::Value> = Vec::new();
|
||||
let nothing = json!([""]);
|
||||
@ -104,11 +104,11 @@ pub async fn get_timetable(url: String) -> Vec<Timetable> {
|
||||
} else if std.is_object() {
|
||||
plan.push(std.clone())
|
||||
}
|
||||
for i in &plan {
|
||||
if i.as_object() != None {
|
||||
courses.push(i.to_owned());
|
||||
for x in &plan {
|
||||
if x.as_object() != None {
|
||||
courses.push(x.to_owned());
|
||||
} else {
|
||||
dbg!("Failed: {:?}", &i);
|
||||
dbg!("Failed to decode plan: {:?}", &i);
|
||||
}
|
||||
}
|
||||
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() {
|
||||
info.push_str(info_value.as_str().unwrap_or(""));
|
||||
}
|
||||
let response = TimetableData {
|
||||
count: plan.len(),
|
||||
courses,
|
||||
info,
|
||||
};
|
||||
let header = xml
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.get("VpMobil")
|
||||
.unwrap()
|
||||
.get("Kopf")
|
||||
.unwrap();
|
||||
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(),
|
||||
),
|
||||
date: String::from(header.get("DatumPlan").unwrap().as_str().unwrap()),
|
||||
updated: String::from(header.get("zeitstempel").unwrap().as_str().unwrap()),
|
||||
class: String::from(
|
||||
i.as_object()
|
||||
.unwrap()
|
||||
@ -170,167 +150,87 @@ pub async fn get_timetable(url: String) -> Vec<Timetable> {
|
||||
.as_str()
|
||||
.unwrap(),
|
||||
),
|
||||
timetable_data: json!(response),
|
||||
timetable_data: json!(TimetableData {
|
||||
count: plan.len(),
|
||||
courses,
|
||||
info,
|
||||
}),
|
||||
};
|
||||
timetable.push(timetable_element)
|
||||
}
|
||||
let normal_classes: Vec<Timetable> = timetable
|
||||
.to_vec()
|
||||
.into_iter()
|
||||
.iter()
|
||||
.cloned()
|
||||
.filter(|e| !e.class.contains("11") && !e.class.contains("12"))
|
||||
.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();
|
||||
timetable_refactored.extend(normal_classes);
|
||||
timetable_refactored.push(eleven);
|
||||
timetable_refactored.push(twelve);
|
||||
for year in ["11", "12"] {
|
||||
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
|
||||
}
|
||||
|
||||
@ -365,15 +265,16 @@ pub async fn get_class_timetable(class: String, url: String) -> TimetableData {
|
||||
info,
|
||||
};
|
||||
for i in classes.iter() {
|
||||
if i.as_object()
|
||||
let current_class = i
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.get("Kurz")
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.replace("/", "_")
|
||||
== class
|
||||
{
|
||||
.replace('/', "_");
|
||||
let contains_class = current_class.contains(&class);
|
||||
if class == current_class {
|
||||
let nothing = json!([""]);
|
||||
let std = i
|
||||
.as_object()
|
||||
@ -399,16 +300,7 @@ pub async fn get_class_timetable(class: String, url: String) -> TimetableData {
|
||||
}
|
||||
}
|
||||
break;
|
||||
} else if class == String::from("11")
|
||||
&& i.as_object()
|
||||
.unwrap()
|
||||
.get("Kurz")
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.replace("/", "_")
|
||||
.contains(&class)
|
||||
{
|
||||
} else if (class == *"11" || class == *"12") && contains_class {
|
||||
let nothing = json!([""]);
|
||||
let std = i
|
||||
.as_object()
|
||||
@ -433,48 +325,28 @@ pub async fn get_class_timetable(class: String, url: String) -> TimetableData {
|
||||
}
|
||||
}
|
||||
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();
|
||||
} else if class == String::from("12")
|
||||
&& i.as_object()
|
||||
fn st_nr(obj: &serde_json::Value) -> i64 {
|
||||
obj.as_object()
|
||||
.unwrap()
|
||||
.get("Kurz")
|
||||
.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()
|
||||
.replace("/", "_")
|
||||
.contains(&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())
|
||||
.unwrap_or_default()
|
||||
.to_owned()
|
||||
}
|
||||
for i in plan {
|
||||
if i.as_object() != None {
|
||||
response.courses.push(i.to_owned());
|
||||
if st_nr(a) == st_nr(b) {
|
||||
fa_abc(a).cmp(&fa_abc(b))
|
||||
} else {
|
||||
dbg!("Failed: {:?}", &i);
|
||||
st_nr(a).cmp(&st_nr(b))
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
@ -486,41 +358,20 @@ pub async fn get_classes() -> Vec<String> {
|
||||
let classes = get_timetable_xml_data(&String::from("Klassen.xml")).await;
|
||||
let mut class_list: Vec<String> = Vec::new();
|
||||
for i in classes.iter() {
|
||||
if String::from(
|
||||
i.as_object()
|
||||
let current_class = i
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.get("Kurz")
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.unwrap(),
|
||||
)
|
||||
.contains("11")
|
||||
{
|
||||
if !class_list.contains(&"11".to_string()) {
|
||||
.unwrap()
|
||||
.to_string();
|
||||
if current_class.contains("11") && !class_list.contains(&"11".to_string()) {
|
||||
class_list.push("11".to_string());
|
||||
}
|
||||
} else if String::from(
|
||||
i.as_object()
|
||||
.unwrap()
|
||||
.get("Kurz")
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.unwrap(),
|
||||
)
|
||||
.contains("12")
|
||||
{
|
||||
if !class_list.contains(&"12".to_string()) {
|
||||
} else if current_class.contains("12") && !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(),
|
||||
));
|
||||
} else if !current_class.contains("11") && !current_class.contains("12") {
|
||||
class_list.push(current_class);
|
||||
}
|
||||
}
|
||||
class_list
|
||||
@ -531,85 +382,16 @@ pub async fn get_class_lessons(class: String) -> Vec<Lesson> {
|
||||
let mut lesson_list: Vec<Lesson> = Vec::new();
|
||||
for i in classes.iter() {
|
||||
let empty_list = serde_json::Value::Array(Vec::new());
|
||||
if i.as_object()
|
||||
let current_class = i
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.get("Kurz")
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.replace("/", "_")
|
||||
== 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)
|
||||
{
|
||||
.replace('/', "_");
|
||||
let contains_class = current_class.contains(&class);
|
||||
if (class == current_class) || ((class == *"11" || class == *"12") && contains_class) {
|
||||
let class_lessons = i
|
||||
.as_object()
|
||||
.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
|
||||
}
|
184
src/main.rs
184
src/main.rs
@ -19,24 +19,74 @@ extern crate rocket;
|
||||
|
||||
mod config;
|
||||
mod indiware_connector;
|
||||
mod keycloak_connector;
|
||||
|
||||
extern crate reqwest;
|
||||
extern crate serde;
|
||||
extern crate serde_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 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)]
|
||||
pub struct Credentials {
|
||||
@ -81,35 +131,43 @@ pub struct Token {
|
||||
struct ApiKey<'r>(&'r str);
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ApiKeyError {
|
||||
enum TokenError {
|
||||
Missing,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for ApiKey<'r> {
|
||||
type Error = ApiKeyError;
|
||||
|
||||
type Error = TokenError;
|
||||
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
/// Returns true if `key` is a valid API key string.
|
||||
fn is_valid(key: &str) -> bool {
|
||||
let mut validation = Validation::default();
|
||||
validation.validate_exp = false;
|
||||
let token = decode::<Claims>(
|
||||
key,
|
||||
&DecodingKey::from_secret(
|
||||
env::var("JWT_SECRET")
|
||||
.unwrap_or(config::JWT_SECRET.to_string())
|
||||
.as_ref(),
|
||||
),
|
||||
&validation,
|
||||
);
|
||||
token.is_ok()
|
||||
async fn request_valid(token: &str) -> Result<reqwest::Response, reqwest::Error> {
|
||||
let client = reqwest::Client::new();
|
||||
client
|
||||
.get(
|
||||
env::var("OIDC_USERINFO").unwrap_or_else(|_| config::OIDC_USERINFO.to_string()),
|
||||
)
|
||||
.header("Authorization", format!("Bearer {}", token))
|
||||
.send()
|
||||
.await
|
||||
}
|
||||
|
||||
fn has_permissions(key: &str, uri: &str) -> bool {
|
||||
let mut validation = Validation::default();
|
||||
validation.validate_exp = false;
|
||||
async fn token_valid(
|
||||
resp: reqwest::Response,
|
||||
) -> 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![
|
||||
String::from("/api/timetable"),
|
||||
String::from("/api/classes"),
|
||||
@ -117,19 +175,9 @@ impl<'r> FromRequest<'r> for ApiKey<'r> {
|
||||
];
|
||||
let student_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();
|
||||
permissions.extend(standard_permissions.iter().cloned());
|
||||
for role in token.claims.roles.iter() {
|
||||
for role in user.roles.iter() {
|
||||
match role {
|
||||
Roles::Admin => permissions.push(String::from("all")),
|
||||
Roles::Student => permissions.extend(student_permissions.iter().cloned()),
|
||||
@ -138,41 +186,53 @@ impl<'r> FromRequest<'r> for ApiKey<'r> {
|
||||
}
|
||||
permissions.contains(&String::from(uri))
|
||||
| permissions.contains(&String::from("all"))
|
||||
| token.claims.whitelist.contains(&String::from(uri))
|
||||
&& !token.claims.blacklist.contains(&String::from(uri))
|
||||
| user
|
||||
.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") {
|
||||
None => Outcome::Failure((Status::Unauthorized, ApiKeyError::Missing)),
|
||||
Some(key)
|
||||
if is_valid(key)
|
||||
&& has_permissions(key, req.route().unwrap().uri.base.path().as_str()) =>
|
||||
None => Outcome::Failure((Status::Unauthorized, TokenError::Missing)),
|
||||
Some(token) => {
|
||||
match request_valid(token).await {
|
||||
Ok(resp) => match token_valid(resp).await {
|
||||
Ok(kc_user) => {
|
||||
if has_permissions(
|
||||
kc_user,
|
||||
req.route().unwrap().uri.base.path().as_str(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Outcome::Success(ApiKey(key))
|
||||
Outcome::Success(ApiKey(token))
|
||||
} else {
|
||||
Outcome::Failure((Status::Unauthorized, 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)),
|
||||
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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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")]
|
||||
@ -230,7 +290,6 @@ async fn get_class_lessons(
|
||||
fn rocket() -> _ {
|
||||
rocket::build()
|
||||
.mount("/", FileServer::from(relative!("static")))
|
||||
.mount("/login", routes![login])
|
||||
.mount(
|
||||
"/api/timetable",
|
||||
routes![
|
||||
@ -242,5 +301,4 @@ fn rocket() -> _ {
|
||||
)
|
||||
.mount("/api/classes", routes![get_classes])
|
||||
.mount("/api/lessons", routes![get_class_lessons])
|
||||
.mount("/api/userinfo", routes![get_userinfo])
|
||||
}
|
||||
|
Reference in New Issue
Block a user