- extended API Endpoints
- restructured API paths - added JWT authorization - added login function (without authentication)
This commit is contained in:
124
src/main.rs
124
src/main.rs
@ -6,6 +6,7 @@ mod schema;
|
||||
|
||||
extern crate serde;
|
||||
|
||||
|
||||
use rocket::serde::json::{Json, json};
|
||||
use crate::schema::timetable;
|
||||
use diesel::{Queryable, Insertable};
|
||||
@ -15,6 +16,13 @@ use reqwest;
|
||||
use quickxml_to_serde::{xml_string_to_json, Config};
|
||||
extern crate serde_json;
|
||||
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")]
|
||||
struct DbConn(PgConnection);
|
||||
@ -34,6 +42,102 @@ struct TimetableData {
|
||||
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 {
|
||||
let client = reqwest::Client::new();
|
||||
let resp = client
|
||||
@ -57,7 +161,7 @@ async fn get_timetable_xml_data() -> Vec<serde_json::value::Value> {
|
||||
}
|
||||
|
||||
#[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 classes = get_timetable_xml_data().await;
|
||||
let mut timetable: Vec<Timetable> = Vec::new();
|
||||
@ -106,7 +210,7 @@ async fn get_timetable(_conn: DbConn) -> Json<Vec<Timetable>> {
|
||||
}
|
||||
|
||||
#[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 courses: Vec<rocket::serde::json::Value> = Vec::new();
|
||||
let mut response = TimetableData {
|
||||
@ -140,8 +244,8 @@ async fn get_class_timetable(_conn: DbConn, class: String) -> Json<TimetableData
|
||||
Json::from(response)
|
||||
}
|
||||
|
||||
#[get("/classes")]
|
||||
async fn get_classes() -> Json<Vec<String>> {
|
||||
#[get("/")]
|
||||
async fn get_classes(_key: ApiKey<'_>) -> Json<Vec<String>> {
|
||||
let classes = get_timetable_xml_data().await;
|
||||
let mut class_list: Vec<String> = Vec::new();
|
||||
for i in classes.iter() {
|
||||
@ -163,10 +267,18 @@ fn hello_name(name: String) -> String {
|
||||
format!("Hello, {}!", name)
|
||||
}
|
||||
|
||||
#[get("/sensitive")]
|
||||
fn sensitive(key: ApiKey<'_>) -> &'static str {
|
||||
"Sensitive data."
|
||||
}
|
||||
|
||||
#[launch]
|
||||
fn rocket() -> _ {
|
||||
rocket::build()
|
||||
.attach(DbConn::fairing())
|
||||
.mount("/hello", routes![hello, hello_name])
|
||||
.mount("/timetable", routes![get_timetable, get_class_timetable, get_classes])
|
||||
.mount("/", FileServer::from(relative!("static")))
|
||||
.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])
|
||||
}
|
||||
|
Reference in New Issue
Block a user