upgrades dependencies, remove unused libs, add licensing information and a correct README

This commit is contained in:
Denys Konovalov 2022-04-16 20:35:06 +02:00
parent 45f51b6774
commit 5db7f250e3
16 changed files with 158 additions and 139 deletions

@ -10,18 +10,10 @@ authors = ["Denys Konovalov <denys.konovalov@protonmail.com>"]
[dependencies] [dependencies]
rocket = { version = "0.5.0-rc.1", features = ["json"] } rocket = { version = "0.5.0-rc.1", features = ["json"] }
serde = "1.0" serde = "1.0"
diesel = { version = "1.4", features = ["postgres", "serde_json"] }
reqwest = { version="0.11", features = ["json"] } reqwest = { version="0.11", features = ["json"] }
quickxml_to_serde = "0.4" quickxml_to_serde = "0.5"
serde_json = "1.0" serde_json = "1.0"
jsonwebtoken = "7.2" serde_derive = "1.0"
time = "0.2" jsonwebtoken = "8.1"
time = "0.3"
chrono = "0.4" chrono = "0.4"
[dependencies.serde_derive]
version = "1.0"
[dependencies.rocket_sync_db_pools]
version = "0.1.0-rc.1"
features = ["diesel_postgres_pool"]

@ -3,13 +3,13 @@ WORKDIR /app
COPY . . COPY . .
RUN cargo install --path . RUN cargo install --path .
FROM debian:buster-slim as runner FROM debian:bullseye-slim as runner
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y ca-certificates tzdata libssl-dev libpq5 \ && apt-get install -y ca-certificates tzdata libssl-dev \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/local/cargo/bin/api /usr/local/bin/api COPY --from=builder /usr/local/cargo/bin/api /usr/local/bin/api
ENV ROCKET_ADDRESS=0.0.0.0 ENV ROCKET_ADDRESS=0.0.0.0
EXPOSE 3000 EXPOSE 8000
CMD ["api"] CMD ["api"]

@ -1,2 +1,70 @@
# 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.
See the repository of the [main application](https://git.cantorgymnasium.de/cantortechnik/meincantor-app) for additional information.
## Building
Dependencies:
- `openssl-devel`
```bash
cargo build --release
```
## Deployment
### docker-compose
```
version: "3.1"
services:
api:
image: lxdb/meincantor-api
restart: always
ports:
- 8000:8000
environment:
IW_TIMETABLE_URL: 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
volumes:
- ./static:/app/static
```
1. Create folder `mkdir meincantor-api`
2. Create `docker-compose.yml` file with the content above
3. Start with `docker-compose up -d`
### local
1. Edit `src/config.rs`
2. Rebuild the binary
3. Execute the binary with `./target/release/api`
## Licensing
```
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/>.
```

@ -1,11 +0,0 @@
[debug]
port = 3000
[debug.databases]
timetable = { url = "postgres://meincantor:meincantor_password@localhost/meincantor_db" }
[release]
port = 3000
[release.databases]
timetable = { url = "postgres://meincantor:meincantor_password@localhost/meincantor_db" }

@ -1,5 +0,0 @@
# For documentation on how to configure this file,
# see diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/schema.rs"

@ -1,16 +1,10 @@
version: "3.1" version: "3.1"
services: services:
postgres:
image: postgres:13-alpine
restart: always
environment:
POSTGRES_PASSWORD: meincantor_password
POSTGRES_USER: meincantor
POSTGRES_DB: meincantor_db
expose: '5432'
api: api:
image: lxdb/meincantor-api image: lxdb/meincantor-api
restart: always restart: always
ports:
- 8000:8000
environment: environment:
IW_TIMETABLE_URL: https://stundenplan24.de/EXAMPLE_SCHOOL/mobil/mobdaten IW_TIMETABLE_URL: https://stundenplan24.de/EXAMPLE_SCHOOL/mobil/mobdaten
IW_TIMETABLE_USER: EXAMPLE_USER IW_TIMETABLE_USER: EXAMPLE_USER
@ -20,6 +14,5 @@ services:
KC_OPENID_TOKEN_ENDPOINT: https://example.keycloak.com/auth/realms/EXAMPLE_REALM/protocol/openid-connect/token 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_OPENID_USERINFO_ENDPOINT: https://example.keycloak.com/auth/realms/EXAMPLE_REALM/protocol/openid-connect/userinfo
KC_CLIENT_ID: EXAMPLE_CLIENT KC_CLIENT_ID: EXAMPLE_CLIENT
ROCKET_DATABASES: '{timetable={url="postgres://meincantor:meincantor_password@postgres/meincantor_db"}}'
volumes: volumes:
- ./static:/app/static - ./static:/app/static

@ -1,6 +0,0 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
DROP FUNCTION IF EXISTS diesel_set_updated_at();

@ -1,36 +0,0 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
-- Sets up a trigger for the given table to automatically set a column called
-- `updated_at` whenever the row is modified (unless `updated_at` was included
-- in the modified columns)
--
-- # Example
--
-- ```sql
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
--
-- SELECT diesel_manage_updated_at('users');
-- ```
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
BEGIN
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
BEGIN
IF (
NEW IS DISTINCT FROM OLD AND
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
) THEN
NEW.updated_at := current_timestamp;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

@ -1,2 +0,0 @@
-- This file should undo anything in `up.sql`
drop table timetable;

@ -1,8 +0,0 @@
-- Your SQL goes here
create table timetable (
id serial primary key,
date varchar(255) not null,
updated varchar(255) not null,
class varchar(255) not null,
timetable_data jsonb
);

@ -1,3 +1,19 @@
// 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/>.
// Timetable source // Timetable source
pub static IW_TIMETABLE_URL: &str = "https://stundenplan24.de/EXAMPLE_SCHOOL/mobil/mobdaten"; pub static IW_TIMETABLE_URL: &str = "https://stundenplan24.de/EXAMPLE_SCHOOL/mobil/mobdaten";
pub static IW_TIMETABLE_USER: &str = "EXAMPLE_USER"; pub static IW_TIMETABLE_USER: &str = "EXAMPLE_USER";

@ -1,14 +1,26 @@
// 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/>.
use crate::config; use crate::config;
use crate::schema::timetable;
use crate::DbConn;
use diesel::{Insertable, Queryable};
use quickxml_to_serde::{xml_string_to_json, Config}; use quickxml_to_serde::{xml_string_to_json, Config};
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use serde_json::{json, Map}; use serde_json::{json, Map};
use std::env; use std::env;
#[derive(Queryable, Serialize, Insertable, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
#[table_name = "timetable"]
pub struct Timetable { pub struct Timetable {
pub date: String, pub date: String,
pub updated: String, pub updated: String,
@ -70,7 +82,7 @@ async fn get_timetable_xml_data(url: &str) -> Vec<serde_json::value::Value> {
classes.to_owned() classes.to_owned()
} }
pub async fn get_timetable(_conn: DbConn, 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();
@ -322,7 +334,7 @@ pub async fn get_timetable(_conn: DbConn, url: String) -> Vec<Timetable> {
timetable_refactored timetable_refactored
} }
pub async fn get_class_timetable(_conn: DbConn, class: String, url: String) -> TimetableData { pub async fn get_class_timetable(class: String, url: String) -> TimetableData {
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 courses: Vec<rocket::serde::json::Value> = Vec::new(); let courses: Vec<rocket::serde::json::Value> = Vec::new();

@ -1,3 +1,19 @@
// 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; extern crate reqwest;
use crate::config; use crate::config;
@ -11,7 +27,7 @@ use std::env;
use std::error::Error; use std::error::Error;
use std::fmt::Display; use std::fmt::Display;
use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::time::{Duration, SystemTime, UNIX_EPOCH};
use time::OffsetDateTime; use time::{macros::format_description, OffsetDateTime};
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct KeycloakAdminToken { pub struct KeycloakAdminToken {
@ -228,7 +244,9 @@ pub async fn login(
TokenStatus::Success => { TokenStatus::Success => {
let userinfo = get_keycloak_userinfo(token.token.clone()).await.unwrap(); let userinfo = get_keycloak_userinfo(token.token.clone()).await.unwrap();
let system_time = OffsetDateTime::now_utc(); let system_time = OffsetDateTime::now_utc();
let datetime = system_time.format("%d/%m/%Y %T"); let datetime = system_time.format(format_description!(
"[day]/[month]/[year] [hour]:[minute]:[second]"
));
let my_claims = Claims { let my_claims = Claims {
iss: env::var("JWT_ISSUER").unwrap_or(config::JWT_ISSUER.to_string()), iss: env::var("JWT_ISSUER").unwrap_or(config::JWT_ISSUER.to_string()),
user: userinfo.preferred_username, user: userinfo.preferred_username,
@ -236,7 +254,7 @@ pub async fn login(
groups: userinfo.groups, groups: userinfo.groups,
blacklist: userinfo.blacklist.unwrap_or_default(), blacklist: userinfo.blacklist.unwrap_or_default(),
whitelist: userinfo.whitelist.unwrap_or_default(), whitelist: userinfo.whitelist.unwrap_or_default(),
jid: (credentials.devid + "@" + &datetime), jid: (credentials.devid + "@" + &datetime.unwrap()),
exp: SystemTime::now() exp: SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.expect("Time went backwards") .expect("Time went backwards")

@ -1,13 +1,25 @@
#[macro_use] // GCG.MeinCantor.API - The server-part of GCG.MeinCantor - The school application for the Georg-Cantor-Gymnasium
extern crate rocket; // 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/>.
#[macro_use] #[macro_use]
extern crate diesel; extern crate rocket;
mod config; mod config;
mod indiware_connector; mod indiware_connector;
mod keycloak_connector; mod keycloak_connector;
mod schema;
extern crate reqwest; extern crate reqwest;
extern crate serde; extern crate serde;
@ -23,13 +35,9 @@ use rocket::{
response::status, response::status,
serde::json::Json, serde::json::Json,
}; };
use rocket_sync_db_pools::{database, diesel::PgConnection};
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use std::env; use std::env;
#[database("timetable")]
pub struct DbConn(PgConnection);
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Credentials { pub struct Credentials {
user: String, user: String,
@ -85,10 +93,8 @@ impl<'r> FromRequest<'r> for ApiKey<'r> {
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> { async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
/// Returns true if `key` is a valid API key string. /// Returns true if `key` is a valid API key string.
fn is_valid(key: &str) -> bool { fn is_valid(key: &str) -> bool {
let validation = Validation { let mut validation = Validation::default();
validate_exp: false, validation.validate_exp = false;
..Default::default()
};
let token = decode::<Claims>( let token = decode::<Claims>(
key, key,
&DecodingKey::from_secret( &DecodingKey::from_secret(
@ -102,10 +108,8 @@ impl<'r> FromRequest<'r> for ApiKey<'r> {
} }
fn has_permissions(key: &str, uri: &str) -> bool { fn has_permissions(key: &str, uri: &str) -> bool {
let validation = Validation { let mut validation = Validation::default();
validate_exp: false, validation.validate_exp = false;
..Default::default()
};
let standard_permissions = vec![ let standard_permissions = vec![
String::from("/api/timetable"), String::from("/api/timetable"),
String::from("/api/classes"), String::from("/api/classes"),
@ -172,44 +176,38 @@ async fn login(
} }
#[get("/latest")] #[get("/latest")]
async fn get_latest_timetable( async fn get_latest_timetable(_key: ApiKey<'_>) -> Json<Vec<timetable_connector::Timetable>> {
conn: DbConn, let timetable = timetable_connector::get_timetable(String::from("Klassen.xml")).await;
_key: ApiKey<'_>,
) -> Json<Vec<timetable_connector::Timetable>> {
let timetable = timetable_connector::get_timetable(conn, String::from("Klassen.xml")).await;
Json::from(timetable) Json::from(timetable)
} }
#[get("/<date>")] #[get("/<date>")]
async fn get_timetable_file( async fn get_timetable_file(
conn: DbConn,
_key: ApiKey<'_>, _key: ApiKey<'_>,
date: String, date: String,
) -> Json<Vec<timetable_connector::Timetable>> { ) -> Json<Vec<timetable_connector::Timetable>> {
let timetable = timetable_connector::get_timetable(conn, format!("PlanKl{}.xml", date)).await; let timetable = timetable_connector::get_timetable(format!("PlanKl{}.xml", date)).await;
Json::from(timetable) Json::from(timetable)
} }
#[get("/latest/<class>")] #[get("/latest/<class>")]
async fn get_latest_class_timetable( async fn get_latest_class_timetable(
conn: DbConn,
class: String, class: String,
_key: ApiKey<'_>, _key: ApiKey<'_>,
) -> Json<timetable_connector::TimetableData> { ) -> Json<timetable_connector::TimetableData> {
let timetable = let timetable =
timetable_connector::get_class_timetable(conn, class, String::from("Klassen.xml")).await; timetable_connector::get_class_timetable(class, String::from("Klassen.xml")).await;
Json::from(timetable) Json::from(timetable)
} }
#[get("/<date>/<class>")] #[get("/<date>/<class>")]
async fn get_class_timetable_file( async fn get_class_timetable_file(
conn: DbConn,
class: String, class: String,
_key: ApiKey<'_>, _key: ApiKey<'_>,
date: String, date: String,
) -> Json<timetable_connector::TimetableData> { ) -> Json<timetable_connector::TimetableData> {
let timetable = let timetable =
timetable_connector::get_class_timetable(conn, class, format!("PlanKl{}.xml", date)).await; timetable_connector::get_class_timetable(class, format!("PlanKl{}.xml", date)).await;
Json::from(timetable) Json::from(timetable)
} }
@ -231,7 +229,6 @@ async fn get_class_lessons(
#[launch] #[launch]
fn rocket() -> _ { fn rocket() -> _ {
rocket::build() rocket::build()
.attach(DbConn::fairing())
.mount("/", FileServer::from(relative!("static"))) .mount("/", FileServer::from(relative!("static")))
.mount("/login", routes![login]) .mount("/login", routes![login])
.mount( .mount(

@ -1,9 +0,0 @@
table! {
timetable (id) {
id -> Int4,
date -> Varchar,
updated -> Varchar,
class -> Varchar,
timetable_data -> Nullable<Jsonb>,
}
}