- extended API Endpoints
- restructured API paths - added JWT authorization - added login function (without authentication)
This commit is contained in:
parent
3d6ac184a4
commit
51d71ce133
@ -11,7 +11,10 @@ serde = "1.0"
|
|||||||
diesel = { version = "1.4", features = ["postgres", "serde_json"] }
|
diesel = { version = "1.4", features = ["postgres", "serde_json"] }
|
||||||
reqwest = "0.11"
|
reqwest = "0.11"
|
||||||
quickxml_to_serde = "0.4"
|
quickxml_to_serde = "0.4"
|
||||||
serde_json = "1.0.64"
|
serde_json = "1.0"
|
||||||
|
jsonwebtoken = "7.2"
|
||||||
|
time = "0.2"
|
||||||
|
|
||||||
|
|
||||||
[dependencies.serde_derive]
|
[dependencies.serde_derive]
|
||||||
version = "1.0"
|
version = "1.0"
|
||||||
|
@ -3,3 +3,10 @@ port = 3000
|
|||||||
|
|
||||||
[debug.databases]
|
[debug.databases]
|
||||||
timetable = { url = "postgres://meincantor:meincantor_password@localhost/meincantor_db" }
|
timetable = { url = "postgres://meincantor:meincantor_password@localhost/meincantor_db" }
|
||||||
|
|
||||||
|
[release]
|
||||||
|
address = "192.168.0.12"
|
||||||
|
port = 3000
|
||||||
|
|
||||||
|
[release.databases]
|
||||||
|
timetable = { url = "postgres://meincantor:meincantor_password@localhost/meincantor_db" }
|
||||||
|
124
src/main.rs
124
src/main.rs
@ -6,6 +6,7 @@ mod schema;
|
|||||||
|
|
||||||
extern crate serde;
|
extern crate serde;
|
||||||
|
|
||||||
|
|
||||||
use rocket::serde::json::{Json, json};
|
use rocket::serde::json::{Json, json};
|
||||||
use crate::schema::timetable;
|
use crate::schema::timetable;
|
||||||
use diesel::{Queryable, Insertable};
|
use diesel::{Queryable, Insertable};
|
||||||
@ -15,6 +16,13 @@ use reqwest;
|
|||||||
use quickxml_to_serde::{xml_string_to_json, Config};
|
use quickxml_to_serde::{xml_string_to_json, Config};
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
mod config;
|
mod config;
|
||||||
|
use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey};
|
||||||
|
use std::time::{SystemTime, Duration, UNIX_EPOCH};
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
use rocket::http::Status;
|
||||||
|
use rocket::request::{self, Outcome, Request, FromRequest};
|
||||||
|
use rocket::fs::{FileServer, relative};
|
||||||
|
|
||||||
|
|
||||||
#[database("timetable")]
|
#[database("timetable")]
|
||||||
struct DbConn(PgConnection);
|
struct DbConn(PgConnection);
|
||||||
@ -34,6 +42,102 @@ struct TimetableData {
|
|||||||
courses: Vec<rocket::serde::json::Value>
|
courses: Vec<rocket::serde::json::Value>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct Credentials {
|
||||||
|
user: String,
|
||||||
|
password: String,
|
||||||
|
devid: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
enum Roles {
|
||||||
|
Student,
|
||||||
|
Teacher,
|
||||||
|
Admin
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct Claims {
|
||||||
|
iss: String,
|
||||||
|
user: String,
|
||||||
|
roles: Vec<Roles>,
|
||||||
|
// permissions: Vec<String>,
|
||||||
|
blacklist: Vec<String>,
|
||||||
|
whitelist: Vec<String>,
|
||||||
|
jid: String,
|
||||||
|
exp: u64
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct Token {
|
||||||
|
token: String
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ApiKey<'r>(&'r str);
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum ApiKeyError {
|
||||||
|
Missing,
|
||||||
|
Invalid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl<'r> FromRequest<'r> for ApiKey<'r> {
|
||||||
|
type Error = ApiKeyError;
|
||||||
|
|
||||||
|
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 token = decode::<Claims>(&key, &DecodingKey::from_secret(config::JWT_SECRET.as_ref()), &Validation::default());
|
||||||
|
token.is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_permissions(key: &str, uri: &str) -> bool {
|
||||||
|
let student_permissions = vec![String::from("/classes"), String::from("/timetable")];
|
||||||
|
let teacher_permissions = vec![String::from("/classes"), String::from("/timetable"), String::from("/t_timetable")];
|
||||||
|
let token = decode::<Claims>(&key, &DecodingKey::from_secret(config::JWT_SECRET.as_ref()), &Validation::default());
|
||||||
|
println!("{:?}", token);
|
||||||
|
let mut token = token.unwrap();
|
||||||
|
let mut permissions = Vec::new();
|
||||||
|
for role in token.claims.roles.iter() {
|
||||||
|
match role {
|
||||||
|
Roles::Admin => permissions.push(String::from("all")),
|
||||||
|
Roles::Student => permissions.extend(student_permissions.iter().cloned()),
|
||||||
|
Roles::Teacher => permissions.extend(teacher_permissions.iter().cloned()),
|
||||||
|
_ => permissions.push(String::from("none"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
permissions.contains(&String::from(uri)) | permissions.contains(&String::from("all")) | &token.claims.whitelist.contains(&String::from(uri)) && !token.claims.blacklist.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.uri().path().as_str()) => Outcome::Success(ApiKey(key)),
|
||||||
|
Some(_) => Outcome::Failure((Status::BadRequest, ApiKeyError::Invalid)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/", data="<credentials>")]
|
||||||
|
fn login(credentials: Json<Credentials>) -> Json<Token> {
|
||||||
|
let credentials = credentials.into_inner();
|
||||||
|
let system_time = OffsetDateTime::now_utc();
|
||||||
|
let datetime = system_time.format("%d/%m/%Y %T");
|
||||||
|
let my_claims = Claims {
|
||||||
|
iss: String::from("Georg-Cantor-Gymnasium Halle(Saale)"),
|
||||||
|
user: credentials.user,
|
||||||
|
roles: vec![Roles::Student, Roles::Admin],
|
||||||
|
// permissions: vec![""]
|
||||||
|
blacklist: vec![String::from("/classes")],
|
||||||
|
whitelist: vec![String::from("/hello/sensitive")],
|
||||||
|
jid: String::from(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 token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret(config::JWT_SECRET.as_ref()));
|
||||||
|
Json(Token { token: token.unwrap() })
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_timetable_xml() -> serde_json::value::Value {
|
async fn get_timetable_xml() -> serde_json::value::Value {
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
let resp = client
|
let resp = client
|
||||||
@ -57,7 +161,7 @@ async fn get_timetable_xml_data() -> Vec<serde_json::value::Value> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
async fn get_timetable(_conn: DbConn) -> Json<Vec<Timetable>> {
|
async fn get_timetable(_conn: DbConn, _key: ApiKey<'_>) -> Json<Vec<Timetable>> {
|
||||||
let xml = get_timetable_xml().await;
|
let xml = get_timetable_xml().await;
|
||||||
let classes = get_timetable_xml_data().await;
|
let classes = get_timetable_xml_data().await;
|
||||||
let mut timetable: Vec<Timetable> = Vec::new();
|
let mut timetable: Vec<Timetable> = Vec::new();
|
||||||
@ -106,7 +210,7 @@ async fn get_timetable(_conn: DbConn) -> Json<Vec<Timetable>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/<class>")]
|
#[get("/<class>")]
|
||||||
async fn get_class_timetable(_conn: DbConn, class: String) -> Json<TimetableData> {
|
async fn get_class_timetable(_conn: DbConn, class: String, _key: ApiKey<'_>) -> Json<TimetableData> {
|
||||||
let classes = get_timetable_xml_data().await;
|
let classes = get_timetable_xml_data().await;
|
||||||
let courses: Vec<rocket::serde::json::Value> = Vec::new();
|
let courses: Vec<rocket::serde::json::Value> = Vec::new();
|
||||||
let mut response = TimetableData {
|
let mut response = TimetableData {
|
||||||
@ -140,8 +244,8 @@ async fn get_class_timetable(_conn: DbConn, class: String) -> Json<TimetableData
|
|||||||
Json::from(response)
|
Json::from(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/classes")]
|
#[get("/")]
|
||||||
async fn get_classes() -> Json<Vec<String>> {
|
async fn get_classes(_key: ApiKey<'_>) -> Json<Vec<String>> {
|
||||||
let classes = get_timetable_xml_data().await;
|
let classes = get_timetable_xml_data().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() {
|
||||||
@ -163,10 +267,18 @@ fn hello_name(name: String) -> String {
|
|||||||
format!("Hello, {}!", name)
|
format!("Hello, {}!", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/sensitive")]
|
||||||
|
fn sensitive(key: ApiKey<'_>) -> &'static str {
|
||||||
|
"Sensitive data."
|
||||||
|
}
|
||||||
|
|
||||||
#[launch]
|
#[launch]
|
||||||
fn rocket() -> _ {
|
fn rocket() -> _ {
|
||||||
rocket::build()
|
rocket::build()
|
||||||
.attach(DbConn::fairing())
|
.attach(DbConn::fairing())
|
||||||
.mount("/hello", routes![hello, hello_name])
|
.mount("/", FileServer::from(relative!("static")))
|
||||||
.mount("/timetable", routes![get_timetable, get_class_timetable, get_classes])
|
.mount("/login", routes![login])
|
||||||
|
.mount("/hello", routes![hello, hello_name, sensitive])
|
||||||
|
.mount("/api/timetable", routes![get_timetable, get_class_timetable])
|
||||||
|
.mount("/api/classes", routes![get_classes])
|
||||||
}
|
}
|
||||||
|
5
static/index.html
Normal file
5
static/index.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>H1</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
1
tutor
1
tutor
@ -1 +0,0 @@
|
|||||||
|
|
@ -1,8 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "xml"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2018"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
@ -1,39 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
use quick_xml::Reader;
|
|
||||||
use quick_xml::events::Event;
|
|
||||||
|
|
||||||
let xml = r#"<tag1 att1 = "test">
|
|
||||||
<tag2><!--Test comment-->Test</tag2>
|
|
||||||
<tag2>
|
|
||||||
Test 2
|
|
||||||
</tag2>
|
|
||||||
</tag1>"#;
|
|
||||||
|
|
||||||
let mut reader = Reader::from_str(xml);
|
|
||||||
reader.trim_text(true);
|
|
||||||
|
|
||||||
let mut count = 0;
|
|
||||||
let mut txt = Vec::new();
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
|
|
||||||
// The `Reader` does not implement `Iterator` because it outputs borrowed data (`Cow`s)
|
|
||||||
loop {
|
|
||||||
match reader.read_event(&mut buf) {
|
|
||||||
Ok(Event::Start(ref e)) => {
|
|
||||||
match e.name() {
|
|
||||||
b"tag1" => println!("attributes values: {:?}",
|
|
||||||
e.attributes().map(|a| a.unwrap().value).collect::<Vec<_>>()),
|
|
||||||
b"tag2" => count += 1,
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Ok(Event::Text(e)) => txt.push(e.unescape_and_decode(&reader).unwrap()),
|
|
||||||
Ok(Event::Eof) => break, // exits the loop when reaching end of file
|
|
||||||
Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e),
|
|
||||||
_ => (), // There are several other `Event`s we do not consider here
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we don't keep a borrow elsewhere, we can clear the buffer to keep memory usage low
|
|
||||||
buf.clear();
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
{"rustc_fingerprint":6590233451751340509,"outputs":{"17598535894874457435":{"success":true,"status":"","code":0,"stdout":"rustc 1.53.0 (53cb7b09b 2021-06-17)\nbinary: rustc\ncommit-hash: 53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b\ncommit-date: 2021-06-17\nhost: x86_64-unknown-linux-gnu\nrelease: 1.53.0\nLLVM version: 12.0.1\n","stderr":""},"931469667778813386":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/denyskon/.rustup/toolchains/stable-x86_64-unknown-linux-gnu\ndebug_assertions\nproc_macro\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""},"2797684049618456168":{"success":false,"status":"exit status: 1","code":1,"stdout":"","stderr":"error: `-Csplit-debuginfo` is unstable on this platform\n\n"}},"successes":{}}
|
|
@ -1,3 +0,0 @@
|
|||||||
Signature: 8a477f597d28d172789f06886806bc55
|
|
||||||
# This file is a cache directory tag created by cargo.
|
|
||||||
# For information about cache directory tags see https://bford.info/cachedir/
|
|
@ -1 +0,0 @@
|
|||||||
This file has an mtime of when this was started.
|
|
@ -1,2 +0,0 @@
|
|||||||
{"message":"expected item, found keyword `let`","code":null,"level":"error","spans":[{"file_name":"src/main.rs","byte_start":56,"byte_end":59,"line_start":6,"line_end":6,"column_start":1,"column_end":4,"is_primary":true,"text":[{"text":"let xml = r#\"<tag1 att1 = \"test\">","highlight_start":1,"highlight_end":4}],"label":"expected item","suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[],"rendered":"\u001b[0m\u001b[1m\u001b[38;5;9merror\u001b[0m\u001b[0m\u001b[1m: expected item, found keyword `let`\u001b[0m\n\u001b[0m \u001b[0m\u001b[0m\u001b[1m\u001b[38;5;12m--> \u001b[0m\u001b[0msrc/main.rs:6:1\u001b[0m\n\u001b[0m \u001b[0m\u001b[0m\u001b[1m\u001b[38;5;12m|\u001b[0m\n\u001b[0m\u001b[1m\u001b[38;5;12m6\u001b[0m\u001b[0m \u001b[0m\u001b[0m\u001b[1m\u001b[38;5;12m| \u001b[0m\u001b[0mlet xml = r#\"<tag1 att1 = \"test\">\u001b[0m\n\u001b[0m \u001b[0m\u001b[0m\u001b[1m\u001b[38;5;12m| \u001b[0m\u001b[0m\u001b[1m\u001b[38;5;9m^^^\u001b[0m\u001b[0m \u001b[0m\u001b[0m\u001b[1m\u001b[38;5;9mexpected item\u001b[0m\n\n"}
|
|
||||||
{"message":"aborting due to previous error","code":null,"level":"error","spans":[],"children":[],"rendered":"\u001b[0m\u001b[1m\u001b[38;5;9merror\u001b[0m\u001b[0m\u001b[1m: aborting due to previous error\u001b[0m\n\n"}
|
|
Reference in New Issue
Block a user