Switch to Untis user import
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
This commit is contained in:
parent
8055a5e4db
commit
735c91f7b4
|
@ -21,6 +21,13 @@ URL=
|
|||
POSTGRES_USER="mw"
|
||||
POSTGRES_PASSWORD=
|
||||
|
||||
# Auth
|
||||
AUTH_UNTIS_URL="https://mese.webuntis.com/WebUntis/"
|
||||
AUTH_UNTIS_CLIENT_NAME="mentorenwahl"
|
||||
AUTH_UNTIS_SCHOOL=
|
||||
AUTH_UNTIS_USERNAME=
|
||||
AUTH_UNTIS_PASSWORD=
|
||||
|
||||
# Backend
|
||||
BACKEND_MINIMUM_TEACHER_SELECTION_COUNT=6
|
||||
BACKEND_ASSIGNMENT_POSSIBILITY_COUNT=65536
|
||||
|
|
|
@ -15,3 +15,4 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
.env
|
||||
data/
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# Mentorenwahl: A fullstack application for assigning mentors to students based on their whishes.
|
||||
# Copyright (C) 2022 Dominic Grimm
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
root = true
|
||||
|
||||
[*.rs]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
dist/
|
||||
vendor/
|
|
@ -0,0 +1,25 @@
|
|||
[package]
|
||||
name = "auth"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4.3.0"
|
||||
anyhow = { version = "1.0.68", features = ["backtrace"] }
|
||||
base64 = "0.21.0"
|
||||
deunicode = "1.3.3"
|
||||
envconfig = "0.10.0"
|
||||
fancy-regex = "0.11.0"
|
||||
futures = "0.3.25"
|
||||
lazy_static = "1.4.0"
|
||||
mime = "0.3.16"
|
||||
serde = { version = "1.0.152", features = ["derive"] }
|
||||
untis = { git = "https://git.dergrimm.net/dergrimm/untis.rs.git", branch = "main" }
|
||||
url = "2.3.1"
|
||||
uuid = { version = "1.2.2", features = ["v4", "fast-rng", "macro-diagnostics"] }
|
||||
wkhtmltopdf = "0.4.0"
|
||||
|
||||
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
||||
tikv-jemallocator = "0.5"
|
|
@ -0,0 +1,42 @@
|
|||
FROM docker.io/lukemathwalker/cargo-chef:latest-rust-1.65.0 as chef
|
||||
|
||||
FROM chef as planner
|
||||
WORKDIR /usr/src/auth
|
||||
RUN mkdir src && touch src/main.rs
|
||||
COPY ./Cargo.toml ./Cargo.lock ./
|
||||
RUN cargo chef prepare --recipe-path recipe.json
|
||||
|
||||
FROM alpine as wkhtmltopdf
|
||||
WORKDIR /tmp
|
||||
RUN wget -O wkhtmltopdf.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.bullseye_amd64.deb
|
||||
|
||||
FROM chef as builder
|
||||
WORKDIR /tmp
|
||||
RUN apt update
|
||||
RUN apt install -y xfonts-base xfonts-75dpi
|
||||
COPY --from=wkhtmltopdf /tmp/wkhtmltopdf.deb .
|
||||
RUN dpkg -i wkhtmltopdf.deb
|
||||
RUN rm wkhtmltopdf.deb
|
||||
RUN apt-get clean
|
||||
RUN apt-get autoremove -y
|
||||
WORKDIR /usr/src/auth
|
||||
COPY --from=planner /usr/src/auth/recipe.json .
|
||||
RUN cargo chef cook --release --recipe-path recipe.json
|
||||
COPY ./src ./src
|
||||
RUN cargo build --release
|
||||
|
||||
FROM docker.io/debian:bullseye-slim as runner
|
||||
WORKDIR /tmp
|
||||
RUN apt update
|
||||
RUN apt install -y ca-certificates
|
||||
RUN apt install -y wget xfonts-base xfonts-75dpi fontconfig libjpeg62-turbo libx11-6 libxcb1 libxext6 libxrender1
|
||||
COPY --from=wkhtmltopdf /tmp/wkhtmltopdf.deb .
|
||||
RUN dpkg -i wkhtmltopdf.deb
|
||||
RUN rm wkhtmltopdf.deb
|
||||
RUN apt-get clean
|
||||
RUN apt-get autoremove -y
|
||||
RUN rm -rf /var/lib/{apt,dpkg,cache,log}/
|
||||
WORKDIR /usr/src/auth
|
||||
COPY --from=builder /usr/src/auth/target/release/auth ./bin/auth
|
||||
EXPOSE 80
|
||||
ENTRYPOINT [ "./bin/auth" ]
|
|
@ -0,0 +1,41 @@
|
|||
use anyhow::Result;
|
||||
use envconfig::Envconfig;
|
||||
use lazy_static::lazy_static;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Envconfig, Debug)]
|
||||
pub struct Config {
|
||||
#[envconfig(from = "AUTH_UNTIS_URL")]
|
||||
pub untis_url: String,
|
||||
|
||||
#[envconfig(from = "AUTH_UNTIS_CLIENT_NAME")]
|
||||
pub untis_client_name: String,
|
||||
|
||||
#[envconfig(from = "AUTH_UNTIS_SCHOOL")]
|
||||
pub untis_school: String,
|
||||
|
||||
#[envconfig(from = "AUTH_UNTIS_USERNAME")]
|
||||
pub untis_username: String,
|
||||
|
||||
#[envconfig(from = "AUTH_UNTIS_PASSWORD")]
|
||||
pub untis_password: String,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref CONFIG: Config = Config::init_from_env().unwrap();
|
||||
}
|
||||
|
||||
pub fn untis_from_env() -> Result<untis::Client> {
|
||||
let webuntis_url = Url::parse(&CONFIG.untis_url)?;
|
||||
|
||||
Ok(untis::Client {
|
||||
rpc_url: untis::Client::gen_rpc_url(&webuntis_url, &CONFIG.untis_school)?,
|
||||
webuntis_url,
|
||||
client_name: CONFIG.untis_client_name.to_owned(),
|
||||
user_agent: "".to_string(),
|
||||
username: CONFIG.untis_username.to_owned(),
|
||||
password: CONFIG.untis_password.to_owned(),
|
||||
session: None,
|
||||
authorization: None,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
pub mod config;
|
||||
pub mod pdf;
|
||||
pub mod users;
|
|
@ -0,0 +1,21 @@
|
|||
#[cfg(not(target_env = "msvc"))]
|
||||
use tikv_jemallocator::Jemalloc;
|
||||
|
||||
#[cfg(not(target_env = "msvc"))]
|
||||
#[global_allocator]
|
||||
static GLOBAL: Jemalloc = Jemalloc;
|
||||
|
||||
use actix_web::{middleware, App, HttpServer};
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
HttpServer::new(|| {
|
||||
App::new()
|
||||
.wrap(middleware::Logger::default())
|
||||
.service(auth::users::get_users)
|
||||
.service(auth::pdf::post_pdf)
|
||||
})
|
||||
.bind(("0.0.0.0", 80))?
|
||||
.run()
|
||||
.await
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
use actix_web::{post, web::Json, HttpResponse};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct PostPdfRequest {
|
||||
pub html: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct PostPdfResponse {
|
||||
pub error: Option<String>,
|
||||
pub filename: Option<String>,
|
||||
}
|
||||
|
||||
#[post("/v1/pdf")]
|
||||
async fn post_pdf(data: Json<PostPdfRequest>) -> HttpResponse {
|
||||
let pdf_app = match wkhtmltopdf::PdfApplication::new() {
|
||||
Ok(x) => x,
|
||||
Err(x) => {
|
||||
return HttpResponse::InternalServerError().json(PostPdfResponse {
|
||||
error: Some(x.to_string()),
|
||||
filename: None,
|
||||
})
|
||||
}
|
||||
};
|
||||
let mut pdfout = match pdf_app
|
||||
.builder()
|
||||
.orientation(wkhtmltopdf::Orientation::Portrait)
|
||||
.margin(wkhtmltopdf::Size::Millimeters(10))
|
||||
.build_from_html(&data.html)
|
||||
{
|
||||
Ok(x) => x,
|
||||
Err(x) => {
|
||||
return HttpResponse::InternalServerError().json(PostPdfResponse {
|
||||
error: Some(x.to_string()),
|
||||
filename: None,
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let start = SystemTime::now();
|
||||
let since_epoch = start
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time went backwards");
|
||||
let filename = format!("/static/{}_{}.pdf", since_epoch.as_secs(), Uuid::new_v4());
|
||||
if let Err(x) = pdfout.save(&filename) {
|
||||
return HttpResponse::InternalServerError().json(PostPdfResponse {
|
||||
error: Some(x.to_string()),
|
||||
filename: None,
|
||||
});
|
||||
}
|
||||
|
||||
HttpResponse::Created().json(PostPdfResponse {
|
||||
error: None,
|
||||
filename: Some(filename),
|
||||
})
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
use actix_web::{get, HttpResponse};
|
||||
use anyhow::Result;
|
||||
use deunicode::deunicode;
|
||||
use fancy_regex::Regex;
|
||||
use lazy_static::lazy_static;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::config;
|
||||
|
||||
lazy_static! {
|
||||
static ref CLASS_REGEX: Regex = Regex::new(r"(9|10)\s?(?!R)[a-z]").unwrap();
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct Class {
|
||||
pub name: String,
|
||||
pub students: Vec<User>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct User {
|
||||
pub username: String,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct GetStudentsResponse {
|
||||
pub error: Option<String>,
|
||||
pub classes: Vec<Class>,
|
||||
pub teachers: Vec<User>,
|
||||
}
|
||||
|
||||
fn escape_username(s: &str) -> String {
|
||||
deunicode(
|
||||
&s.to_lowercase()
|
||||
.chars()
|
||||
.filter(|c| !c.is_whitespace())
|
||||
.collect::<String>(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn students(
|
||||
client: &mut untis::Client,
|
||||
usernames: &mut HashMap<String, usize>,
|
||||
) -> Result<Vec<Class>> {
|
||||
Ok(futures::future::try_join_all(
|
||||
client
|
||||
.classes()
|
||||
.await?
|
||||
.iter()
|
||||
.filter(|c| CLASS_REGEX.is_match(&c.name).unwrap())
|
||||
.map(|c| async {
|
||||
Ok::<_, anyhow::Error>(Class {
|
||||
name: c.name.to_owned(),
|
||||
students: client
|
||||
.student_reports(c.id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|s| User {
|
||||
username: escape_username(&s.name),
|
||||
first_name: s.fore_name,
|
||||
last_name: s.long_name,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}),
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|c| Class {
|
||||
name: c.name,
|
||||
students: c
|
||||
.students
|
||||
.into_iter()
|
||||
.map(|s| User {
|
||||
username: {
|
||||
let escaped = escape_username(&s.username);
|
||||
match usernames.get_mut(&escaped) {
|
||||
Some(x) => {
|
||||
*x += 1;
|
||||
format!("{}{}", escaped, x)
|
||||
}
|
||||
None => {
|
||||
usernames.insert(escaped.to_owned(), 1);
|
||||
escaped
|
||||
}
|
||||
}
|
||||
},
|
||||
first_name: s.first_name,
|
||||
last_name: s.last_name,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn teachers(
|
||||
client: &mut untis::Client,
|
||||
usernames: &mut HashMap<String, usize>,
|
||||
) -> Result<Vec<User>> {
|
||||
Ok(client
|
||||
.teacher_reports()
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter_map(|t| {
|
||||
if t.long_name.starts_with("Abi-Aufsicht")
|
||||
|| t.long_name == "SBBZ"
|
||||
|| t.long_name == "N.N."
|
||||
|| t.long_name == "Werkrealschule"
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(User {
|
||||
username: {
|
||||
let escaped = escape_username(&t.name);
|
||||
match usernames.get_mut(&escaped) {
|
||||
Some(x) => {
|
||||
*x += 1;
|
||||
format!("{}{}", escaped, x)
|
||||
}
|
||||
None => {
|
||||
usernames.insert(escaped.to_owned(), 1);
|
||||
escaped
|
||||
}
|
||||
}
|
||||
},
|
||||
first_name: t.fore_name,
|
||||
last_name: t.long_name,
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[get("/v1/users")]
|
||||
pub async fn get_users() -> HttpResponse {
|
||||
let mut client = match config::untis_from_env() {
|
||||
Ok(x) => x,
|
||||
Err(x) => {
|
||||
return HttpResponse::InternalServerError().json(GetStudentsResponse {
|
||||
error: Some(x.to_string()),
|
||||
classes: vec![],
|
||||
teachers: vec![],
|
||||
})
|
||||
}
|
||||
};
|
||||
if let Err(x) = client.login().await {
|
||||
return HttpResponse::InternalServerError().json(GetStudentsResponse {
|
||||
error: Some(x.to_string()),
|
||||
classes: vec![],
|
||||
teachers: vec![],
|
||||
});
|
||||
}
|
||||
|
||||
let mut usernames = HashMap::<String, usize>::new();
|
||||
let classes = match students(&mut client, &mut usernames).await {
|
||||
Ok(x) => x,
|
||||
Err(x) => {
|
||||
return HttpResponse::InternalServerError().json(GetStudentsResponse {
|
||||
error: Some(x.to_string()),
|
||||
classes: vec![],
|
||||
teachers: vec![],
|
||||
})
|
||||
}
|
||||
};
|
||||
let teachers = match teachers(&mut client, &mut usernames).await {
|
||||
Ok(x) => x,
|
||||
Err(x) => {
|
||||
return HttpResponse::InternalServerError().json(GetStudentsResponse {
|
||||
error: Some(x.to_string()),
|
||||
classes: vec![],
|
||||
teachers: vec![],
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(x) = client.logout().await {
|
||||
return HttpResponse::InternalServerError().json(GetStudentsResponse {
|
||||
error: Some(x.to_string()),
|
||||
classes: vec![],
|
||||
teachers: vec![],
|
||||
});
|
||||
}
|
||||
|
||||
HttpResponse::Ok()
|
||||
.content_type(mime::APPLICATION_JSON)
|
||||
.json(GetStudentsResponse {
|
||||
error: None,
|
||||
classes,
|
||||
teachers,
|
||||
})
|
||||
}
|
|
@ -14,12 +14,14 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
FROM crystallang/crystal:1.6.2-alpine as micrate-deps
|
||||
FROM docker.io/crystallang/crystal:1.6.2-alpine as crystal
|
||||
|
||||
FROM crystal as micrate-deps
|
||||
WORKDIR /usr/src/micrate
|
||||
COPY ./micrate/shard.yml ./micrate/shard.lock ./
|
||||
RUN shards install --production
|
||||
|
||||
FROM crystallang/crystal:1.6.2-alpine as micrate-builder
|
||||
FROM crystal as micrate-builder
|
||||
WORKDIR /usr/src/micrate
|
||||
COPY --from=micrate-deps /usr/src/micrate/shard.yml /usr/src/micrate/shard.lock ./
|
||||
COPY --from=micrate-deps /usr/src/micrate/lib ./lib
|
||||
|
@ -31,12 +33,12 @@ WORKDIR /usr/src/public
|
|||
COPY ./public ./src
|
||||
RUN minify -r -o ./dist ./src
|
||||
|
||||
FROM crystallang/crystal:1.6.2-alpine as deps
|
||||
FROM crystal as deps
|
||||
WORKDIR /usr/src/mentorenwahl
|
||||
COPY ./shard.yml ./shard.lock ./
|
||||
RUN shards install --production
|
||||
|
||||
FROM crystallang/crystal:1.6.2-alpine as builder
|
||||
FROM crystal as builder
|
||||
WORKDIR /usr/src/mentorenwahl
|
||||
RUN apk add --no-cache pcre2-dev
|
||||
RUN mkdir deps
|
||||
|
|
|
@ -22,6 +22,9 @@ CREATE TYPE user_roles AS ENUM ('student', 'teacher');
|
|||
CREATE TABLE users(
|
||||
id serial PRIMARY KEY,
|
||||
username text UNIQUE NOT NULL,
|
||||
password_hash text NOT NULL,
|
||||
first_name text NOT NULL,
|
||||
last_name text NOT NULL,
|
||||
role user_roles NOT NULL,
|
||||
admin boolean NOT NULL
|
||||
);
|
||||
|
@ -40,9 +43,12 @@ CREATE TABLE teachers(
|
|||
max_students int NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE classes(id serial PRIMARY KEY, name text UNIQUE NOT NULL);
|
||||
|
||||
CREATE TABLE students(
|
||||
id serial PRIMARY KEY,
|
||||
user_id int NOT NULL UNIQUE REFERENCES users(id)
|
||||
user_id int NOT NULL UNIQUE REFERENCES users(id),
|
||||
class_id int NOT NULL REFERENCES classes(id)
|
||||
);
|
||||
|
||||
CREATE TABLE votes(
|
||||
|
@ -78,6 +84,8 @@ DROP TABLE teachers;
|
|||
|
||||
DROP TABLE students;
|
||||
|
||||
DROP TABLE classes;
|
||||
|
||||
DROP TABLE tokens;
|
||||
|
||||
DROP TABLE users;
|
||||
|
|
|
@ -8,7 +8,7 @@ targets:
|
|||
micrate:
|
||||
main: src/micrate.cr
|
||||
|
||||
crystal: 1.6.2
|
||||
crystal: 1.7.1
|
||||
|
||||
dependencies:
|
||||
micrate:
|
||||
|
|
|
@ -152,7 +152,15 @@ shards:
|
|||
git: https://github.com/maiha/shard.cr.git
|
||||
version: 1.0.0
|
||||
|
||||
tallboy:
|
||||
git: https://github.com/epoch/tallboy.git
|
||||
version: 0.9.3
|
||||
|
||||
version_from_shard:
|
||||
git: https://github.com/hugopl/version_from_shard.git
|
||||
version: 1.2.5
|
||||
|
||||
wannabe_bool:
|
||||
git: https://github.com/llamicron/wannabe_bool.git
|
||||
version: 0.1.0+git.commit.a64a71a091094d0ba88cf6b81598aa268656ece3
|
||||
|
||||
|
|
|
@ -69,3 +69,7 @@ dependencies:
|
|||
git: https://git.dergrimm.net/dergrimm/compiled_license.git
|
||||
docker:
|
||||
github: repomaa/docker.cr
|
||||
tallboy:
|
||||
github: epoch/tallboy
|
||||
wannabe_bool:
|
||||
github: llamicron/wannabe_bool
|
||||
|
|
|
@ -24,9 +24,6 @@ module Backend
|
|||
module Api
|
||||
# GraphQL request context class
|
||||
class Context < GraphQL::Context
|
||||
# Development mode
|
||||
getter development
|
||||
|
||||
# Request status
|
||||
getter status
|
||||
|
||||
|
@ -46,7 +43,6 @@ module Backend
|
|||
getter jti
|
||||
|
||||
def initialize(
|
||||
@development : Bool,
|
||||
@status : Status,
|
||||
@user : Db::User?,
|
||||
@admin : Bool?,
|
||||
|
@ -56,7 +52,7 @@ module Backend
|
|||
)
|
||||
end
|
||||
|
||||
def initialize(headers : HTTP::Headers, @development : Bool, @status = Status::OK, *rest)
|
||||
def initialize(headers : HTTP::Headers, @status = Status::OK, *rest)
|
||||
super(*rest)
|
||||
|
||||
if (token = headers["authorization"]?) && token.starts_with?(Auth::BEARER)
|
||||
|
@ -89,9 +85,7 @@ module Backend
|
|||
|
||||
def on_development : Nil
|
||||
{% if !flag?(:release) %}
|
||||
if @development
|
||||
yield
|
||||
end
|
||||
{% end %}
|
||||
end
|
||||
|
||||
|
@ -191,21 +185,13 @@ module Backend
|
|||
ex.api_message
|
||||
when Errors::PrivateError
|
||||
{% if !flag?(:release) %}
|
||||
if @development
|
||||
ex.api_message
|
||||
else
|
||||
Errors::UNKNOWN_PRIVATE_ERROR
|
||||
end
|
||||
{% else %}
|
||||
Errors::UNKNOWN_PRIVATE_ERROR
|
||||
{% end %}
|
||||
else
|
||||
{% if !flag?(:release) %}
|
||||
if @development
|
||||
ex.message || Errors::UNKNOWN_PRIVATE_ERROR
|
||||
else
|
||||
Errors::UNKNOWN_PRIVATE_ERROR
|
||||
end
|
||||
{% else %}
|
||||
Errors::UNKNOWN_PRIVATE_ERROR
|
||||
{% end %}
|
||||
|
|
|
@ -28,8 +28,8 @@ module Backend
|
|||
def login(username : String, password : String) : LoginPayload?
|
||||
raise Errors::Authentication.new if username.empty? || password.empty?
|
||||
|
||||
user = Db::User.query.find { var(:username) == username }
|
||||
raise Errors::Authentication.new unless user && Ldap.authenticate?(Ldap::DN.uid(username), password)
|
||||
user = Db::User.query.find { var(:username) == username.downcase }
|
||||
raise Errors::Authentication.new unless user && user.password_hash.verify(password)
|
||||
|
||||
token = Db::Token.create!(
|
||||
id: UUID.random(Random::Secure),
|
||||
|
@ -73,24 +73,24 @@ module Backend
|
|||
Scalars::UUID.new(token.value)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
# Creates user
|
||||
def create_user(context : Context, input : UserCreateInput, check_ldap : Bool = true) : User?
|
||||
context.admin!
|
||||
# @[GraphQL::Field]
|
||||
# # Creates user
|
||||
# def create_user(context : Context, input : UserCreateInput, check_ldap : Bool = true) : User?
|
||||
# context.admin!
|
||||
|
||||
raise Errors::LdapUserDoesNotExist.new if check_ldap && begin
|
||||
!Ldap::User.from_username(input.username)
|
||||
rescue LDAP::Client::AuthError
|
||||
true
|
||||
end
|
||||
user = Db::User.create!(username: input.username, role: input.role.to_db, admin: input.admin)
|
||||
if input.role.student?
|
||||
Db::Student.create!(user_id: user.id)
|
||||
end
|
||||
Worker::Jobs::CacheLdapUserJob.new(user.id.not_nil!.to_i).enqueue
|
||||
# raise Errors::LdapUserDoesNotExist.new if check_ldap && begin
|
||||
# !Ldap::User.from_username(input.username)
|
||||
# rescue LDAP::Client::AuthError
|
||||
# true
|
||||
# end
|
||||
# user = Db::User.create!(username: input.username, role: input.role.to_db, admin: input.admin)
|
||||
# if input.role.student?
|
||||
# Db::Student.create!(user_id: user.id)
|
||||
# end
|
||||
# Worker::Jobs::CacheLdapUserJob.new(user.id.not_nil!.to_i).enqueue
|
||||
|
||||
User.new(user)
|
||||
end
|
||||
# User.new(user)
|
||||
# end
|
||||
|
||||
@[GraphQL::Field]
|
||||
# Deletes user by ID
|
||||
|
@ -122,14 +122,14 @@ module Backend
|
|||
# Teacher.new(teacher)
|
||||
# end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def register_teacher(context : Context, input : TeacherInput) : Teacher?
|
||||
context.teacher!(false)
|
||||
raise Errors::InvalidPermissions.new if context.user.not_nil!.teacher
|
||||
# @[GraphQL::Field]
|
||||
# def register_teacher(context : Context, input : TeacherInput) : Teacher?
|
||||
# context.teacher!(false)
|
||||
# raise Errors::InvalidPermissions.new if context.user.not_nil!.teacher
|
||||
|
||||
teacher = Db::Teacher.create!(user_id: context.user.not_nil!.id, max_students: input.max_students)
|
||||
Teacher.new(teacher)
|
||||
end
|
||||
# teacher = Db::Teacher.create!(user_id: context.user.not_nil!.id, max_students: input.max_students)
|
||||
# Teacher.new(teacher)
|
||||
# end
|
||||
|
||||
# @[GraphQL::Field]
|
||||
# # Deletes teacher by ID
|
||||
|
@ -142,15 +142,15 @@ module Backend
|
|||
# id
|
||||
# end
|
||||
|
||||
# @[GraphQL::Field]
|
||||
# # Self register as teacher
|
||||
# def register_teacher(context : Context, input : TeacherInput) : Teacher
|
||||
# context.teacher! external_check: false
|
||||
@[GraphQL::Field]
|
||||
# Self register as teacher
|
||||
def register_teacher(context : Context, input : TeacherInput) : Teacher
|
||||
context.teacher! external_check: false
|
||||
|
||||
# Teacher.new(
|
||||
# Db::Teacher.create!(user_id: context.user.not_nil!.id, max_students: input.max_students)
|
||||
# )
|
||||
# end
|
||||
Teacher.new(
|
||||
Db::Teacher.create!(user_id: context.user.not_nil!.id, max_students: input.max_students)
|
||||
)
|
||||
end
|
||||
|
||||
# @[GraphQL::Field]
|
||||
# # Creates student
|
||||
|
|
|
@ -46,34 +46,41 @@ module Backend
|
|||
|
||||
db_object Db::User, Int32
|
||||
|
||||
@ldap : Ldap::User?
|
||||
# @ldap : Ldap::User?
|
||||
|
||||
# LDAP user data
|
||||
def ldap : Ldap::User
|
||||
if @ldap.nil? && (raw_cache = Redis::CLIENT.get("ldap:user:#{id}")).nil?
|
||||
Worker::Jobs::CacheLdapUserJob.new(id).perform
|
||||
raw_cache = Redis::CLIENT.get("ldap:user:#{id}").not_nil!
|
||||
end
|
||||
# # LDAP user data
|
||||
# def ldap : Ldap::User
|
||||
# if @ldap.nil? && (raw_cache = Redis::CLIENT.get("ldap:user:#{id}")).nil?
|
||||
# Worker::Jobs::CacheLdapUserJob.new(id).perform
|
||||
# raw_cache = Redis::CLIENT.get("ldap:user:#{id}").not_nil!
|
||||
# end
|
||||
|
||||
(@ldap ||= Ldap::User.from_json(raw_cache.not_nil!)).not_nil!
|
||||
end
|
||||
# (@ldap ||= Ldap::User.from_json(raw_cache.not_nil!)).not_nil!
|
||||
# end
|
||||
|
||||
@[GraphQL::Field]
|
||||
# User's first name
|
||||
def first_name : String
|
||||
ldap.first_name
|
||||
# ldap.first_name
|
||||
@model.first_name
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
# User's last name
|
||||
def last_name : String
|
||||
ldap.last_name
|
||||
# ldap.last_name
|
||||
@model.last_name
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
# User's full name
|
||||
def name(formal : Bool = true) : String
|
||||
ldap.name(formal)
|
||||
# ldap.name(formal)
|
||||
if formal
|
||||
"#{@model.last_name}, #{@model.first_name}"
|
||||
else
|
||||
"#{@model.first_name} #{@model.last_name}"
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
|
@ -82,12 +89,6 @@ module Backend
|
|||
@model.username
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
# User's email
|
||||
def email : String
|
||||
ldap.email
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
# User is admin
|
||||
def admin : Bool
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
require "http/client"
|
||||
|
||||
module Backend::Auth
|
||||
extend self
|
||||
|
||||
struct UsersResponse
|
||||
include JSON::Serializable
|
||||
|
||||
struct Class
|
||||
include JSON::Serializable
|
||||
|
||||
getter name : String
|
||||
getter students : Array(User)
|
||||
end
|
||||
|
||||
struct User
|
||||
include JSON::Serializable
|
||||
|
||||
getter username : String
|
||||
getter first_name : String
|
||||
getter last_name : String
|
||||
end
|
||||
|
||||
getter error : String?
|
||||
getter classes : Array(Class)
|
||||
getter teachers : Array(User)
|
||||
end
|
||||
|
||||
struct GeneratePdfResponse
|
||||
include JSON::Serializable
|
||||
|
||||
getter error : String?
|
||||
getter filename : String?
|
||||
end
|
||||
|
||||
def users : UsersResponse
|
||||
resp = HTTP::Client.get(Path[Backend.config.auth.url, "users"].to_s)
|
||||
data = UsersResponse.from_json(resp.body)
|
||||
raise "Error in response (#{resp.status_code}): #{data.error}" if resp.status_code != 200
|
||||
|
||||
data
|
||||
end
|
||||
|
||||
def generate_pdf(html : String) : GeneratePdfResponse
|
||||
resp = HTTP::Client.post(Path[Backend.config.auth.url, "pdf"].to_s, body: {:html => html}.to_json)
|
||||
data = GeneratePdfResponse.from_json(resp.body)
|
||||
raise "Error in response (#{resp.status_code}): #{data.error}" if resp.status_code != 201
|
||||
|
||||
data
|
||||
end
|
||||
end
|
|
@ -1,4 +1,6 @@
|
|||
require "commander"
|
||||
require "tallboy"
|
||||
require "wannabe_bool"
|
||||
|
||||
module Backend
|
||||
CLI = Commander::Command.new do |cmd|
|
||||
|
@ -60,41 +62,160 @@ module Backend
|
|||
end
|
||||
|
||||
cmd.commands.add do |c|
|
||||
c.use = "register <username> <role>"
|
||||
c.short = "Seeds the database with required data"
|
||||
c.use = "seed"
|
||||
c.short = "Imports users from Untis"
|
||||
c.long = c.short
|
||||
|
||||
c.flags.add do |f|
|
||||
f.name = "admin"
|
||||
f.long = "--admin"
|
||||
f.default = false
|
||||
f.description = "Register as admin"
|
||||
c.run do
|
||||
users = Auth.users
|
||||
|
||||
students = [] of Templates::Users::Student
|
||||
users.classes.each do |cl|
|
||||
c_db = Db::Class.create!({name: cl.name})
|
||||
cl.students.each do |s|
|
||||
password = Password.generate(Password::DEFAULT_LEN)
|
||||
students << Templates::Users::Student.new(
|
||||
class_name: cl.name,
|
||||
user: Templates::Users::User.new(
|
||||
first_name: s.first_name,
|
||||
last_name: s.last_name,
|
||||
username: s.username,
|
||||
password: password
|
||||
)
|
||||
)
|
||||
user = Db::User.create!({
|
||||
username: s.username,
|
||||
password: password,
|
||||
first_name: s.first_name,
|
||||
last_name: s.last_name,
|
||||
role: Db::UserRole::Student,
|
||||
admin: false,
|
||||
})
|
||||
Db::Student.create!({
|
||||
user_id: user.id,
|
||||
class_id: c_db.id,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
c.flags.add do |f|
|
||||
f.name = "yes"
|
||||
f.short = "-y"
|
||||
f.long = "--yes"
|
||||
f.default = false
|
||||
f.description = "Answer yes to all questions"
|
||||
teachers = [] of Templates::Users::User
|
||||
Db::User.import(
|
||||
users.teachers.map do |t|
|
||||
password = Password.generate(Password::DEFAULT_LEN)
|
||||
teachers << Templates::Users::User.new(
|
||||
first_name: t.first_name,
|
||||
last_name: t.last_name,
|
||||
username: t.username,
|
||||
password: password
|
||||
)
|
||||
|
||||
Db::User.new({
|
||||
username: t.username,
|
||||
password: password,
|
||||
first_name: t.first_name,
|
||||
last_name: t.last_name,
|
||||
role: Db::UserRole::Teacher,
|
||||
admin: false,
|
||||
})
|
||||
end
|
||||
)
|
||||
|
||||
html = Templates::Users.new(students, teachers).to_s
|
||||
puts "Filepath: #{Auth.generate_pdf(html).filename}"
|
||||
end
|
||||
end
|
||||
|
||||
c.run do |opts, args|
|
||||
username = args[0]
|
||||
role = Db::UserRole.from_string(args[1].underscore)
|
||||
unless opts.bool["yes"]
|
||||
print "Register '#{username}' as '#{role.to_api}'#{opts.bool["admin"] ? " with admin privileges" : nil}? [y/N] "
|
||||
abort unless gets(chomp: true).not_nil!.strip.downcase == "y"
|
||||
# cmd.commands.add do |c|
|
||||
# c.use = "register <username> <role>"
|
||||
# c.short = "Seeds the database with required data"
|
||||
# c.long = c.short
|
||||
|
||||
# c.flags.add do |f|
|
||||
# f.name = "admin"
|
||||
# f.long = "--admin"
|
||||
# f.default = false
|
||||
# f.description = "Register as admin"
|
||||
# end
|
||||
|
||||
# c.flags.add do |f|
|
||||
# f.name = "yes"
|
||||
# f.short = "-y"
|
||||
# f.long = "--yes"
|
||||
# f.default = false
|
||||
# f.description = "Answer yes to all questions"
|
||||
# end
|
||||
|
||||
# c.run do |opts, args|
|
||||
# username = args[0]
|
||||
# role = Db::UserRole.from_string(args[1].underscore)
|
||||
# unless opts.bool["yes"]
|
||||
# print "Register '#{username}' as '#{role.to_api}'#{opts.bool["admin"] ? " with admin privileges" : nil}? [y/N] "
|
||||
# abort unless gets(chomp: true).not_nil!.strip.downcase == "y"
|
||||
# end
|
||||
|
||||
# user = Db::User.create!(username: username, role: role.to_s, admin: opts.bool["admin"])
|
||||
# if role == Db::UserRole::Student
|
||||
# Db::Student.create!(user_id: user.id)
|
||||
# end
|
||||
|
||||
# # Worker::Jobs::CacheLdapUserJob.new(user.id).enqueue
|
||||
|
||||
# puts "Done!"
|
||||
# end
|
||||
# end
|
||||
|
||||
# ameba:disable Lint/ShadowingOuterLocalVar
|
||||
cmd.commands.add do |cmd|
|
||||
cmd.use = "user:list"
|
||||
cmd.short = "Lists all users"
|
||||
cmd.long = cmd.short
|
||||
|
||||
cmd.run do
|
||||
users = Db::User.query.to_a
|
||||
table = Tallboy.table do
|
||||
header ["id", "username", "first_name", "last_name", "role", "admin"]
|
||||
|
||||
users.each do |user|
|
||||
row [
|
||||
user.id,
|
||||
user.username,
|
||||
user.first_name,
|
||||
user.last_name,
|
||||
user.role,
|
||||
user.admin,
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
user = Db::User.create!(username: username, role: role.to_s, admin: opts.bool["admin"])
|
||||
if role == Db::UserRole::Student
|
||||
Db::Student.create!(user_id: user.id)
|
||||
puts table
|
||||
end
|
||||
end
|
||||
|
||||
Worker::Jobs::CacheLdapUserJob.new(user.id).enqueue
|
||||
# ameba:disable Lint/ShadowingOuterLocalVar
|
||||
cmd.commands.add do |cmd|
|
||||
cmd.use = "user:admin <id> <admin>"
|
||||
cmd.short = "Gives or removed admin rights"
|
||||
cmd.long = cmd.short
|
||||
|
||||
puts "Done!"
|
||||
cmd.run do |_opts, args|
|
||||
user = Db::User.find!(args[0].to_i)
|
||||
user.admin = args[1].to_b
|
||||
user.save!
|
||||
|
||||
table = Tallboy.table do
|
||||
header ["id", "username", "first_name", "last_name", "role", "admin"]
|
||||
|
||||
row [
|
||||
user.id,
|
||||
user.username,
|
||||
user.first_name,
|
||||
user.last_name,
|
||||
user.role,
|
||||
user.admin,
|
||||
]
|
||||
end
|
||||
|
||||
puts table
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -83,6 +83,10 @@ module Backend
|
|||
# Configuration for `Ldap`
|
||||
getter ldap : LdapConfig
|
||||
|
||||
@[EnvConfig::Setting(key: "auth")]
|
||||
# Configuration for authorization provider
|
||||
getter auth : AuthConfig
|
||||
|
||||
# Configuration for `Api`
|
||||
class ApiConfig
|
||||
include EnvConfig
|
||||
|
@ -178,5 +182,13 @@ module Backend
|
|||
# Periodical cache refresh interval
|
||||
getter cache_refresh_interval : Int32
|
||||
end
|
||||
|
||||
# Configuration for authoriuation API
|
||||
class AuthConfig
|
||||
include EnvConfig
|
||||
|
||||
# Auth API URL
|
||||
getter url : String
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
class Backend::Db::Class
|
||||
include Clear::Model
|
||||
self.table = :classes
|
||||
|
||||
primary_key type: :serial
|
||||
|
||||
column name : String
|
||||
|
||||
has_many students : Student, foreign_key: :class_id
|
||||
end
|
|
@ -6,6 +6,7 @@ module Backend::Db
|
|||
primary_key type: :serial
|
||||
|
||||
belongs_to user : User
|
||||
belongs_to class_model : Class, foreign_key: :class_id
|
||||
|
||||
has_one vote : Vote?, foreign_key: :student_id
|
||||
has_one assignment : Assignment?, foreign_key: :student_id
|
||||
|
|
|
@ -27,6 +27,9 @@ module Backend::Db
|
|||
primary_key type: :serial
|
||||
|
||||
column username : String
|
||||
column password_hash : Crypto::Bcrypt::Password
|
||||
column first_name : String
|
||||
column last_name : String
|
||||
column role : UserRole
|
||||
column admin : Bool = false
|
||||
|
||||
|
@ -34,5 +37,9 @@ module Backend::Db
|
|||
has_one teacher : Teacher?, foreign_key: :user_id
|
||||
|
||||
has_many tokens : Token, foreign_key: :user_id
|
||||
|
||||
def password=(x : String)
|
||||
self.password_hash = Crypto::Bcrypt::Password.create(x)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
module Backend::Password
|
||||
extend self
|
||||
|
||||
DEFAULT_LEN = 10_u32
|
||||
DEFAULT_CHARSET = {
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
|
||||
}
|
||||
|
||||
def generate(len : UInt32) : String
|
||||
String.build(len) do |s|
|
||||
len.times do
|
||||
c = DEFAULT_CHARSET.sample(Random::Secure)
|
||||
s << (((Random::Secure.rand(UInt8) & 1) == 1) ? c.upcase : c)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -38,6 +38,7 @@ module Backend
|
|||
def run : self
|
||||
{% if !flag?(:release) %}
|
||||
Log.warn { "Backend is running in development mode! Do not use this in production!" }
|
||||
pp Backend.config
|
||||
{% end %}
|
||||
|
||||
Log.info { "Checking if DB schema is up to date..." }
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
require "ecr"
|
||||
|
||||
require "./templates/*"
|
||||
|
||||
module Backend::Templates
|
||||
end
|
|
@ -0,0 +1,29 @@
|
|||
class Backend::Templates::Users
|
||||
struct User
|
||||
property first_name
|
||||
property last_name
|
||||
property username
|
||||
property password
|
||||
|
||||
def initialize(
|
||||
@first_name : String,
|
||||
@last_name : String,
|
||||
@username : String,
|
||||
@password : String
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
struct Student
|
||||
property class_name
|
||||
property user
|
||||
|
||||
def initialize(@class_name : String, @user : User)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(@students : Array(Student), @teachers : Array(User))
|
||||
end
|
||||
|
||||
ECR.def_to_s "#{__DIR__}/users.ecr"
|
||||
end
|
|
@ -0,0 +1,132 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Benutzeraccounts | Mentorenwahl</title>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.column {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.border {
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.padded {
|
||||
padding: 1%;
|
||||
}
|
||||
|
||||
.left,
|
||||
.right {
|
||||
float: left;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-top: 1px solid;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: monospace;
|
||||
background: #f4f4f4;
|
||||
word-wrap: break-word;
|
||||
box-decoration-break: clone;
|
||||
padding: 0.1rem 0.3rem 0.2rem;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<p>Schüler:</p>
|
||||
<table width="100%" cellspacing="0" border="0">
|
||||
<colgroup>
|
||||
<col class="column" />
|
||||
<col class="column" />
|
||||
<col class="column" />
|
||||
<col class="column" />
|
||||
</colgroup>
|
||||
|
||||
<%- @students.in_groups_of(4).each do |group| -%>
|
||||
<tr>
|
||||
<%- group.each do |student| %>
|
||||
<td class="border padded">
|
||||
<%- if student -%>
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td>
|
||||
<%= student.user.last_name %>, <%= student.user.first_name %> (<%= student.class_name %>)
|
||||
<hr />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code><%= student.user.username %></code>
|
||||
<hr />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code><%= student.user.password %></code>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<%- end -%>
|
||||
</td>
|
||||
<%- end -%>
|
||||
</tr>
|
||||
<%- end -%>
|
||||
</table>
|
||||
<footer></footer>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Lehrer:</p>
|
||||
<table width="100%" cellspacing="0" border="0">
|
||||
<colgroup>
|
||||
<col class="column" />
|
||||
<col class="column" />
|
||||
<col class="column" />
|
||||
<col class="column" />
|
||||
</colgroup>
|
||||
|
||||
<%- @teachers.in_groups_of(4).each do |group| -%>
|
||||
<tr>
|
||||
<%- group.each do |teacher| %>
|
||||
<td class="border padded">
|
||||
<%- if teacher -%>
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td>
|
||||
<%= teacher.last_name %>, <%= teacher.first_name %>
|
||||
<hr />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code><%= teacher.username %></code>
|
||||
<hr />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code><%= teacher.password %></code>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<%- end -%>
|
||||
</td>
|
||||
<%- end -%>
|
||||
</tr>
|
||||
<%- end -%>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -59,21 +59,14 @@ module Backend
|
|||
end
|
||||
|
||||
@[ARTA::Post("")]
|
||||
{% if !flag?(:release) %}
|
||||
@[ATHA::QueryParam("development", description: "Enables development mode")]
|
||||
{% end %}
|
||||
def endpoint(request : ATH::Request, development : Bool = false) : ATH::Exceptions::BadRequest | ATH::Response
|
||||
{% if !flag?(:release) %}
|
||||
Log.notice { "Development request icoming" } if development
|
||||
{% end %}
|
||||
|
||||
def endpoint(request : ATH::Request) : ATH::Exceptions::BadRequest | ATH::Response
|
||||
return ATH::Exceptions::BadRequest.new("No request body given") unless request.body
|
||||
query = GraphQLQuery.from_json(request.body.not_nil!)
|
||||
|
||||
ATH::StreamedResponse.new(
|
||||
headers: HTTP::Headers{
|
||||
"content-type" => "application/json",
|
||||
"cache-control" => ["no-cache", "no-store", "max-age=0", "must-revalidate"],
|
||||
"cache-control" => {"no-cache", "no-store", "max-age=0", "must-revalidate"},
|
||||
}
|
||||
) do |io|
|
||||
Api::Schema::SCHEMA.execute(
|
||||
|
@ -81,7 +74,7 @@ module Backend
|
|||
query.query,
|
||||
query.variables,
|
||||
query.operation_name,
|
||||
Api::Context.new(request.headers, development)
|
||||
Api::Context.new(request.headers)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -32,8 +32,6 @@ services:
|
|||
db:
|
||||
image: docker.io/postgres:alpine
|
||||
restart: always
|
||||
networks:
|
||||
- db
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
|
@ -43,20 +41,29 @@ services:
|
|||
adminer:
|
||||
image: docker.io/adminer:standalone
|
||||
restart: always
|
||||
networks:
|
||||
- default
|
||||
- db
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
restart: always
|
||||
networks:
|
||||
- redis
|
||||
volumes:
|
||||
- redis:/data
|
||||
|
||||
auth:
|
||||
image: git.dergrimm.net/mentorenwahl/auth:latest
|
||||
build:
|
||||
context: ./auth
|
||||
restart: always
|
||||
environment:
|
||||
AUTH_UNTIS_URL: ${AUTH_UNTIS_URL}
|
||||
AUTH_UNTIS_CLIENT_NAME: ${AUTH_UNTIS_CLIENT_NAME}
|
||||
AUTH_UNTIS_SCHOOL: ${AUTH_UNTIS_SCHOOL}
|
||||
AUTH_UNTIS_USERNAME: ${AUTH_UNTIS_USERNAME}
|
||||
AUTH_UNTIS_PASSWORD: ${AUTH_UNTIS_PASSWORD}
|
||||
volumes:
|
||||
- ./data/static:/static
|
||||
|
||||
backend:
|
||||
image: git.dergrimm.net/mentorenwahl/backend:latest
|
||||
build:
|
||||
|
@ -64,13 +71,10 @@ services:
|
|||
args:
|
||||
BUILD_ENV: production
|
||||
restart: always
|
||||
networks:
|
||||
- default
|
||||
- db
|
||||
- redis
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
- auth
|
||||
environment:
|
||||
BACKEND_URL: ${URL}
|
||||
BACKEND_MINIMUM_TEACHER_SELECTION_COUNT: ${BACKEND_MINIMUM_TEACHER_SELECTION_COUNT}
|
||||
|
@ -94,22 +98,16 @@ services:
|
|||
BACKEND_LDAP_BIND_DN: ${BACKEND_LDAP_BIND_DN}
|
||||
BACKEND_LDAP_BIND_PASSWORD: ${BACKEND_LDAP_BIND_PASSWORD}
|
||||
BACKEND_LDAP_CACHE_REFRESH_INTERVAL: ${BACKEND_LDAP_CACHE_REFRESH_INTERVAL}
|
||||
BACKEND_AUTH_URL: "http://auth/v1"
|
||||
|
||||
frontend:
|
||||
image: git.dergrimm.net/mentorenwahl/frontend:latest
|
||||
build:
|
||||
context: ./frontend
|
||||
restart: always
|
||||
networks:
|
||||
- default
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
networks:
|
||||
db:
|
||||
redis:
|
||||
|
||||
|
||||
volumes:
|
||||
db:
|
||||
redis:
|
||||
|
|
|
@ -6,7 +6,6 @@ query Students {
|
|||
firstName
|
||||
lastName
|
||||
username
|
||||
email
|
||||
role
|
||||
admin
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ query Teachers {
|
|||
firstName
|
||||
lastName
|
||||
username
|
||||
email
|
||||
role
|
||||
admin
|
||||
}
|
||||
|
|
|
@ -71,7 +71,6 @@ scalar UUID
|
|||
|
||||
type User {
|
||||
admin: Boolean!
|
||||
email: String!
|
||||
externalId: Int!
|
||||
firstName: String!
|
||||
id: Int!
|
||||
|
@ -162,12 +161,11 @@ type LoginPayload {
|
|||
}
|
||||
|
||||
type Mutation {
|
||||
createUser(checkLdap: Boolean! = true, input: UserCreateInput!): User
|
||||
createVote(input: VoteCreateInput!): Vote
|
||||
deleteUser(id: Int!): Int
|
||||
login(password: String!, username: String!): LoginPayload
|
||||
logout: UUID
|
||||
registerTeacher(input: TeacherInput!): Teacher
|
||||
registerTeacher(input: TeacherInput!): Teacher!
|
||||
revokeToken(token: UUID!): UUID!
|
||||
startAssignment: Boolean
|
||||
}
|
||||
|
@ -176,12 +174,6 @@ input TeacherInput {
|
|||
maxStudents: Int!
|
||||
}
|
||||
|
||||
input UserCreateInput {
|
||||
username: String!
|
||||
role: UserRole!
|
||||
admin: Boolean! = false
|
||||
}
|
||||
|
||||
input VoteCreateInput {
|
||||
teacherIds: [Int!]!
|
||||
}
|
|
@ -20,7 +20,7 @@ impl Component for NewUserModal {
|
|||
type Message = Msg;
|
||||
type Properties = NewUserModalProps;
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self {
|
||||
username: NodeRef::default(),
|
||||
admin: NodeRef::default(),
|
||||
|
|
|
@ -153,8 +153,7 @@ impl Component for Users {
|
|||
<th><abbr title="ID des Benutzers in der Datenbank / API">{ "ID" }</abbr></th>
|
||||
<th>{ "Nachname" }</th>
|
||||
<th>{ "Vorname" }</th>
|
||||
<th><abbr title="LDAP Benutzername">{ "Benutzername" }</abbr></th>
|
||||
<th>{ "Email" }</th>
|
||||
<th>{ "Benutzername" }</th>
|
||||
<th>{ "Rolle" }</th>
|
||||
<th><abbr title="ID des externen Benutzerobjekts">{ "Externe Rollen-ID" }</abbr></th>
|
||||
<th>{ "Admin" }</th>
|
||||
|
@ -169,11 +168,7 @@ impl Component for Users {
|
|||
<td>{ &s.user.last_name }</td>
|
||||
<td>{ &s.user.first_name }</td>
|
||||
<td><code>{ &s.user.username }</code></td>
|
||||
<td>
|
||||
<a href={format!("mailto:{}", s.user.email)}>
|
||||
<code>{ &s.user.email }</code>
|
||||
</a>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<code>
|
||||
{
|
||||
|
@ -198,11 +193,6 @@ impl Component for Users {
|
|||
<td>{ &t.user.last_name }</td>
|
||||
<td>{ &t.user.first_name }</td>
|
||||
<td><code>{ &t.user.username }</code></td>
|
||||
<td>
|
||||
<a href={format!("mailto:{}", t.user.email)}>
|
||||
<code>{ &t.user.email }</code>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<code>
|
||||
{
|
||||
|
@ -230,8 +220,7 @@ impl Component for Users {
|
|||
<th><abbr title="ID des Schülers in der Datenbank / API">{ "ID" }</abbr></th>
|
||||
<th>{ "Nachname" }</th>
|
||||
<th>{ "Vorname" }</th>
|
||||
<th><abbr title="LDAP Benutzername">{ "Benutzername" }</abbr></th>
|
||||
<th>{ "Email" }</th>
|
||||
<th>{ "Benutzername" }</th>
|
||||
<th>{ "Benutzer-ID" }</th>
|
||||
<th>{ "Admin" }</th>
|
||||
<th>{ "Gewählt" }</th>
|
||||
|
@ -245,11 +234,6 @@ impl Component for Users {
|
|||
<td>{ &s.user.last_name }</td>
|
||||
<td>{ &s.user.first_name }</td>
|
||||
<td><code>{ &s.user.username }</code></td>
|
||||
<td>
|
||||
<a href={format!("mailto:{}", s.user.email)}>
|
||||
<code>{ &s.user.email }</code>
|
||||
</a>
|
||||
</td>
|
||||
<td><code>{ &s.user.id }</code></td>
|
||||
<td><code>{ if s.user.admin { 1 } else { 0 } }</code></td>
|
||||
<td><code>{ if s.vote.is_some() { 1 } else { 0 } }</code></td>
|
||||
|
@ -266,7 +250,7 @@ impl Component for Users {
|
|||
<th><abbr title="ID des Lehrers in der Datenbank / API">{ "ID" }</abbr></th>
|
||||
<th>{ "Nachname" }</th>
|
||||
<th>{ "Vorname" }</th>
|
||||
<th><abbr title="LDAP Benutzername">{ "Benutzername" }</abbr></th>
|
||||
<th>{ "Benutzername" }</th>
|
||||
<th>{ "Email" }</th>
|
||||
<th>{ "Benutzer-ID" }</th>
|
||||
<th>{ "Admin" }</th>
|
||||
|
@ -280,11 +264,6 @@ impl Component for Users {
|
|||
<td>{ &t.user.last_name }</td>
|
||||
<td>{ &t.user.first_name }</td>
|
||||
<td><code>{ &t.user.username }</code></td>
|
||||
<td>
|
||||
<a href={format!("mailto:{}", t.user.email)}>
|
||||
<code>{ &t.user.email }</code>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<code>
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue