This commit is contained in:
parent
1c72c81b85
commit
50379148bc
34
.drone.yml
34
.drone.yml
|
@ -43,25 +43,25 @@ steps:
|
||||||
- name: ameba
|
- name: ameba
|
||||||
image: veelenga/ameba
|
image: veelenga/ameba
|
||||||
commands:
|
commands:
|
||||||
- cd docker/backend
|
- cd backend
|
||||||
- ameba micrate/src src
|
- ameba micrate/src src
|
||||||
- name: deps
|
- name: deps
|
||||||
image: crystallang/crystal:1.3-alpine
|
image: crystallang/crystal:1.6-alpine
|
||||||
volumes:
|
volumes:
|
||||||
- name: lib
|
- name: lib
|
||||||
path: /drone/src/docker/backend/lib
|
path: /drone/src/backend/lib
|
||||||
- name: cache
|
- name: cache
|
||||||
path: /cache
|
path: /cache
|
||||||
commands:
|
commands:
|
||||||
- cd docker/backend
|
- cd backend
|
||||||
- shards install
|
- shards install
|
||||||
- name: docs
|
- name: docs
|
||||||
image: crystallang/crystal:1.3-alpine
|
image: crystallang/crystal:1.6-alpine
|
||||||
volumes:
|
volumes:
|
||||||
- name: lib
|
- name: lib
|
||||||
path: /drone/src/docker/backend/lib
|
path: /drone/src/backend/lib
|
||||||
commands:
|
commands:
|
||||||
- cd docker/backend
|
- cd backend
|
||||||
- make docs
|
- make docs
|
||||||
depends_on:
|
depends_on:
|
||||||
- ameba
|
- ameba
|
||||||
|
@ -74,7 +74,7 @@ steps:
|
||||||
settings:
|
settings:
|
||||||
rebuild: true
|
rebuild: true
|
||||||
mount:
|
mount:
|
||||||
- ./docker/backend/docs
|
- ./backend/docs
|
||||||
depends_on:
|
depends_on:
|
||||||
- docs
|
- docs
|
||||||
- name: build
|
- name: build
|
||||||
|
@ -83,20 +83,10 @@ steps:
|
||||||
- name: dockersock
|
- name: dockersock
|
||||||
path: /var/run/docker.sock
|
path: /var/run/docker.sock
|
||||||
commands:
|
commands:
|
||||||
- docker-compose build --build-arg BUILD_ENV=development backend
|
- docker-compose build backend
|
||||||
depends_on:
|
depends_on:
|
||||||
- ameba
|
- ameba
|
||||||
|
|
||||||
volumes:
|
|
||||||
- name: lib
|
|
||||||
temp: {}
|
|
||||||
- name: cache
|
|
||||||
host:
|
|
||||||
path: /tmp/cache
|
|
||||||
- name: dockersock
|
|
||||||
host:
|
|
||||||
path: /var/run/docker.sock
|
|
||||||
|
|
||||||
---
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
type: docker
|
type: docker
|
||||||
|
@ -109,7 +99,7 @@ steps:
|
||||||
- name: dockersock
|
- name: dockersock
|
||||||
path: /var/run/docker.sock
|
path: /var/run/docker.sock
|
||||||
commands:
|
commands:
|
||||||
- docker-compose build --build-arg BUILD_ENV=development frontend
|
- docker-compose build frontend
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- name: dockersock
|
- name: dockersock
|
||||||
|
@ -131,7 +121,7 @@ steps:
|
||||||
restore: true
|
restore: true
|
||||||
mount:
|
mount:
|
||||||
- ./docs/book
|
- ./docs/book
|
||||||
- ./docker/backend/docs
|
- ./backend/docs
|
||||||
- name: prepare-pages
|
- name: prepare-pages
|
||||||
image: bitnami/git
|
image: bitnami/git
|
||||||
volumes:
|
volumes:
|
||||||
|
@ -144,7 +134,7 @@ steps:
|
||||||
- rm -rf /tmp/pages/*
|
- rm -rf /tmp/pages/*
|
||||||
- cp -r ./docs/book/* /tmp/pages
|
- cp -r ./docs/book/* /tmp/pages
|
||||||
- mkdir -p /tmp/pages/_api/backend
|
- mkdir -p /tmp/pages/_api/backend
|
||||||
- cp -r ./docker/backend/docs/* /tmp/pages/_api/backend
|
- cp -r ./backend/docs/* /tmp/pages/_api/backend
|
||||||
depends_on:
|
depends_on:
|
||||||
- restore-cache
|
- restore-cache
|
||||||
- name: deploy-pages
|
- name: deploy-pages
|
||||||
|
|
|
@ -26,7 +26,6 @@ BACKEND_MINIMUM_TEACHER_SELECTION_COUNT=6
|
||||||
BACKEND_ASSIGNMENT_POSSIBILITY_COUNT=500
|
BACKEND_ASSIGNMENT_POSSIBILITY_COUNT=500
|
||||||
BACKEND_URL=URL
|
BACKEND_URL=URL
|
||||||
# Backend - API
|
# Backend - API
|
||||||
BACKEND_API_GRAPHQL_PLAYGROUND=false
|
|
||||||
BACKEND_API_JWT_SECRET=
|
BACKEND_API_JWT_SECRET=
|
||||||
BACKEND_API_JWT_EXPIRATION=360
|
BACKEND_API_JWT_EXPIRATION=360
|
||||||
# Backend - Worker
|
# Backend - Worker
|
||||||
|
|
|
@ -14,50 +14,66 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
FROM crystallang/crystal:1.5-alpine as micrate-deps
|
FROM veelenga/ameba:latest
|
||||||
WORKDIR /src
|
WORKDIR /usr/src/micrate
|
||||||
|
COPY ./micrate/src ./src
|
||||||
|
RUN ameba src/ || true
|
||||||
|
|
||||||
|
FROM crystallang/crystal:1.6-alpine as micrate-deps
|
||||||
|
WORKDIR /usr/src/micrate
|
||||||
COPY ./micrate/shard.yml ./micrate/shard.lock ./
|
COPY ./micrate/shard.yml ./micrate/shard.lock ./
|
||||||
RUN shards install --production
|
RUN shards install --production
|
||||||
|
|
||||||
FROM crystallang/crystal:1.5-alpine as micrate-builder
|
FROM crystallang/crystal:1.6-alpine as micrate-builder
|
||||||
WORKDIR /src
|
WORKDIR /usr/src/micrate
|
||||||
# RUN apk add --no-cache sqlite-static
|
COPY --from=micrate-deps /usr/src/micrate/shard.yml /usr/src/micrate/shard.lock ./
|
||||||
COPY --from=micrate-deps /src/shard.yml /src/shard.lock ./
|
COPY --from=micrate-deps /usr/src/micrate/lib ./lib
|
||||||
COPY --from=micrate-deps /src/lib ./lib
|
|
||||||
COPY ./micrate/src ./src
|
COPY ./micrate/src ./src
|
||||||
RUN shards build --release --static --verbose -s -p -t
|
RUN shards build --release --static --verbose -s -p -t
|
||||||
|
|
||||||
FROM tdewolff/minify as public
|
FROM tdewolff/minify as public
|
||||||
WORKDIR /src
|
WORKDIR /usr/src/public
|
||||||
COPY ./public ./src
|
COPY ./public ./src
|
||||||
RUN minify -r -o ./dist ./src
|
RUN minify -r -o ./dist ./src
|
||||||
|
|
||||||
FROM crystallang/crystal:1.5-alpine as deps
|
FROM veelenga/ameba:latest
|
||||||
WORKDIR /src
|
WORKDIR /usr/src/mentorenwahl
|
||||||
|
COPY ./src ./src
|
||||||
|
RUN ameba src/ || true
|
||||||
|
|
||||||
|
FROM crystallang/crystal:1.6-alpine as deps
|
||||||
|
WORKDIR /usr/src/mentorenwahl
|
||||||
COPY ./shard.yml ./shard.lock ./
|
COPY ./shard.yml ./shard.lock ./
|
||||||
RUN shards install --production
|
RUN shards install --production
|
||||||
|
|
||||||
FROM crystallang/crystal:1.5-alpine as builder
|
FROM crystallang/crystal:1.6-alpine as builder
|
||||||
ARG BUILD_ENV
|
WORKDIR /usr/src/mentorenwahl
|
||||||
WORKDIR /src/mentorenwahl
|
|
||||||
RUN apk add --no-cache pcre2-dev
|
RUN apk add --no-cache pcre2-dev
|
||||||
COPY --from=deps /src/shard.yml /src/shard.lock ./
|
COPY --from=deps /usr/src/mentorenwahl/shard.yml /usr/src/mentorenwahl/shard.lock ./
|
||||||
COPY --from=deps /src/lib ./lib
|
COPY --from=deps /usr/src/mentorenwahl/lib ./lib
|
||||||
COPY ./LICENSE .
|
COPY ./LICENSE .
|
||||||
COPY ./Makefile .
|
COPY ./Makefile .
|
||||||
COPY ./src ./src
|
|
||||||
COPY ./db ./db
|
COPY ./db ./db
|
||||||
COPY --from=public /src/dist ./public
|
COPY ./src ./src
|
||||||
|
ARG BUILD_ENV
|
||||||
|
COPY --from=public /usr/src/public/dist ./public
|
||||||
RUN if [ "${BUILD_ENV}" = "development" ]; then \
|
RUN if [ "${BUILD_ENV}" = "development" ]; then \
|
||||||
make dev; \
|
make dev; \
|
||||||
else \
|
else \
|
||||||
make; \
|
make; \
|
||||||
fi
|
fi
|
||||||
|
RUN mkdir deps
|
||||||
|
RUN if [ "${BUILD_ENV}" = "development" ]; then \
|
||||||
|
ldd bin/backend | tr -s '[:blank:]' '\n' | grep '^/' | \
|
||||||
|
xargs -I % sh -c 'mkdir -p $(dirname deps%); cp % deps%;'; \
|
||||||
|
fi
|
||||||
|
|
||||||
FROM scratch as runner
|
FROM scratch as runner
|
||||||
COPY --from=micrate-builder /src/bin/micrate /bin/micrate
|
WORKDIR /
|
||||||
COPY --from=builder /src/mentorenwahl/bin /bin
|
COPY --from=micrate-builder /usr/src/micrate/bin/micrate ./bin/micrate
|
||||||
COPY --from=builder /src/mentorenwahl/db ./db
|
COPY --from=builder /usr/src/mentorenwahl/deps /
|
||||||
|
COPY --from=builder /usr/src/mentorenwahl/bin/backend ./bin/backend
|
||||||
|
COPY --from=builder /usr/src/mentorenwahl/db ./db
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
ENTRYPOINT [ "backend" ]
|
ENTRYPOINT [ "./bin/backend" ]
|
||||||
CMD [ "run" ]
|
CMD [ "run" ]
|
|
@ -19,10 +19,10 @@
|
||||||
all: prod
|
all: prod
|
||||||
|
|
||||||
dev:
|
dev:
|
||||||
shards build -Ddevelopment --static --verbose -s -p -t
|
shards build -Dplayground --verbose -s -p -t
|
||||||
|
|
||||||
prod:
|
prod:
|
||||||
shards build --static --release --verbose -s -p -t
|
shards build --production --static --release --verbose -s -p -t
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
crystal docs --project-name "Mentorenwahl Backend"
|
crystal docs --project-name "Mentorenwahl"
|
|
@ -17,14 +17,14 @@
|
||||||
*/
|
*/
|
||||||
-- +micrate Up
|
-- +micrate Up
|
||||||
-- SQL in section ' Up ' is executed when this migration is applied
|
-- SQL in section ' Up ' is executed when this migration is applied
|
||||||
CREATE TYPE user_roles AS ENUM ('teacher', 'student');
|
CREATE TYPE user_roles AS ENUM ('student', 'teacher');
|
||||||
|
|
||||||
CREATE TABLE users(
|
CREATE TABLE users(
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
username TEXT UNIQUE NOT NULL,
|
username TEXT UNIQUE NOT NULL,
|
||||||
role user_roles NOT NULL,
|
role user_roles NOT NULL,
|
||||||
skif BOOLEAN NOT NULL,
|
admin BOOLEAN NOT NULL,
|
||||||
admin BOOLEAN NOT NULL
|
jti uuid UNIQUE
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE teachers(
|
CREATE TABLE teachers(
|
|
@ -6,7 +6,7 @@ shards:
|
||||||
|
|
||||||
athena:
|
athena:
|
||||||
git: https://github.com/athena-framework/framework.git
|
git: https://github.com/athena-framework/framework.git
|
||||||
version: 0.17.0
|
version: 0.17.1
|
||||||
|
|
||||||
athena-config:
|
athena-config:
|
||||||
git: https://github.com/athena-framework/config.git
|
git: https://github.com/athena-framework/config.git
|
||||||
|
@ -30,15 +30,15 @@ shards:
|
||||||
|
|
||||||
athena-routing:
|
athena-routing:
|
||||||
git: https://github.com/athena-framework/routing.git
|
git: https://github.com/athena-framework/routing.git
|
||||||
version: 0.1.2
|
version: 0.1.3
|
||||||
|
|
||||||
athena-serializer:
|
athena-serializer:
|
||||||
git: https://github.com/athena-framework/serializer.git
|
git: https://github.com/athena-framework/serializer.git
|
||||||
version: 0.3.0
|
version: 0.3.1
|
||||||
|
|
||||||
athena-validator:
|
athena-validator:
|
||||||
git: https://github.com/athena-framework/validator.git
|
git: https://github.com/athena-framework/validator.git
|
||||||
version: 0.2.0
|
version: 0.2.1
|
||||||
|
|
||||||
baked_file_system:
|
baked_file_system:
|
||||||
git: https://github.com/schovi/baked_file_system.git
|
git: https://github.com/schovi/baked_file_system.git
|
||||||
|
@ -46,7 +46,7 @@ shards:
|
||||||
|
|
||||||
bindata:
|
bindata:
|
||||||
git: https://github.com/spider-gazelle/bindata.git
|
git: https://github.com/spider-gazelle/bindata.git
|
||||||
version: 1.10.0
|
version: 1.11.0
|
||||||
|
|
||||||
clear:
|
clear:
|
||||||
git: https://github.com/vici37/clear.git
|
git: https://github.com/vici37/clear.git
|
||||||
|
@ -82,7 +82,7 @@ shards:
|
||||||
|
|
||||||
graphql:
|
graphql:
|
||||||
git: https://github.com/graphql-crystal/graphql.git
|
git: https://github.com/graphql-crystal/graphql.git
|
||||||
version: 0.4.0
|
version: 0.4.0+git.commit.e3281bb0ef0ca301ccea176e6839422ac766465b
|
||||||
|
|
||||||
habitat:
|
habitat:
|
||||||
git: https://github.com/luckyframework/habitat.git
|
git: https://github.com/luckyframework/habitat.git
|
||||||
|
@ -126,7 +126,7 @@ shards:
|
||||||
|
|
||||||
pretty:
|
pretty:
|
||||||
git: https://github.com/maiha/pretty.cr.git
|
git: https://github.com/maiha/pretty.cr.git
|
||||||
version: 1.1.1
|
version: 1.1.2
|
||||||
|
|
||||||
promise:
|
promise:
|
||||||
git: https://github.com/spider-gazelle/promise.git
|
git: https://github.com/spider-gazelle/promise.git
|
||||||
|
@ -150,7 +150,7 @@ shards:
|
||||||
|
|
||||||
shard:
|
shard:
|
||||||
git: https://github.com/maiha/shard.cr.git
|
git: https://github.com/maiha/shard.cr.git
|
||||||
version: 0.3.1
|
version: 1.0.0
|
||||||
|
|
||||||
version_from_shard:
|
version_from_shard:
|
||||||
git: https://github.com/hugopl/version_from_shard.git
|
git: https://github.com/hugopl/version_from_shard.git
|
|
@ -24,9 +24,9 @@ license: GNU GPLv3
|
||||||
|
|
||||||
targets:
|
targets:
|
||||||
backend:
|
backend:
|
||||||
main: src/cli/backend.cr
|
main: src/backend.cr
|
||||||
|
|
||||||
crystal: 1.5.0
|
crystal: 1.6.1
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
clear:
|
clear:
|
||||||
|
@ -34,6 +34,7 @@ dependencies:
|
||||||
branch: master
|
branch: master
|
||||||
graphql:
|
graphql:
|
||||||
github: graphql-crystal/graphql
|
github: graphql-crystal/graphql
|
||||||
|
branch: main
|
||||||
jwt:
|
jwt:
|
||||||
github: crystal-community/jwt
|
github: crystal-community/jwt
|
||||||
commander:
|
commander:
|
|
@ -14,8 +14,15 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
require "commander"
|
||||||
|
require "docker"
|
||||||
|
|
||||||
require "./backend/*"
|
require "./backend/*"
|
||||||
|
|
||||||
# Base module
|
# Base module
|
||||||
module Backend
|
module Backend
|
||||||
|
Docker.setup
|
||||||
|
Db.init
|
||||||
|
|
||||||
|
Commander.run(CLI, ARGV)
|
||||||
end
|
end
|
82
backend/src/backend/api/auth.cr
Normal file
82
backend/src/backend/api/auth.cr
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
# 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/>.
|
||||||
|
|
||||||
|
require "jwt"
|
||||||
|
require "json"
|
||||||
|
|
||||||
|
module Backend
|
||||||
|
module Api
|
||||||
|
# Authorization and authentication utilities
|
||||||
|
module Auth
|
||||||
|
extend self
|
||||||
|
|
||||||
|
# Bearer token header
|
||||||
|
BEARER = "Bearer "
|
||||||
|
|
||||||
|
# JWT token
|
||||||
|
struct Token
|
||||||
|
include JSON::Serializable
|
||||||
|
|
||||||
|
getter iss : String
|
||||||
|
getter iat : Int64
|
||||||
|
getter exp : Int64
|
||||||
|
getter jti : String
|
||||||
|
getter context : Context
|
||||||
|
|
||||||
|
def initialize(
|
||||||
|
@iss : String,
|
||||||
|
@iat : Int64,
|
||||||
|
@exp : Int64,
|
||||||
|
@jti : String,
|
||||||
|
@context : Context
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def encode : String
|
||||||
|
JWT.encode(self, Backend.config.api.jwt_secret, JWT::Algorithm::HS256)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.from_hash(token : Hash(String, JSON::Any)) : self
|
||||||
|
self.new(
|
||||||
|
iss: token["iss"].as_s,
|
||||||
|
iat: token["iat"].as_i64,
|
||||||
|
exp: token["exp"].as_i64,
|
||||||
|
jti: token["jti"].as_s,
|
||||||
|
context: Context.from_hash(token["context"].as_h)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.decode(jwt : String) : self
|
||||||
|
self.from_hash(JWT.decode(jwt, Backend.config.api.jwt_secret, JWT::Algorithm::HS256)[0].as_h)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# JWT token context data
|
||||||
|
struct Context
|
||||||
|
include JSON::Serializable
|
||||||
|
|
||||||
|
getter user : Int32
|
||||||
|
|
||||||
|
def initialize(@user : Int32)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.from_hash(data : Hash(String, JSON::Any))
|
||||||
|
self.new(user: data["user"].as_i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
require "graphql"
|
require "graphql"
|
||||||
require "http/headers"
|
require "http/headers"
|
||||||
|
require "jwt/errors"
|
||||||
|
require "json"
|
||||||
|
|
||||||
module Backend
|
module Backend
|
||||||
module Api
|
module Api
|
||||||
|
@ -24,6 +26,9 @@ module Backend
|
||||||
# Development mode
|
# Development mode
|
||||||
getter development
|
getter development
|
||||||
|
|
||||||
|
# Request status
|
||||||
|
getter status
|
||||||
|
|
||||||
# Authenticated user
|
# Authenticated user
|
||||||
getter user
|
getter user
|
||||||
|
|
||||||
|
@ -38,6 +43,7 @@ module Backend
|
||||||
|
|
||||||
def initialize(
|
def initialize(
|
||||||
@development : Bool,
|
@development : Bool,
|
||||||
|
@status : Status,
|
||||||
@user : Db::User?,
|
@user : Db::User?,
|
||||||
@admin : Bool?,
|
@admin : Bool?,
|
||||||
@role : Schema::UserRole?,
|
@role : Schema::UserRole?,
|
||||||
|
@ -45,37 +51,54 @@ module Backend
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(headers : HTTP::Headers, @development : Bool, *rest)
|
def initialize(headers : HTTP::Headers, @development : Bool, @status = Status::OK, *rest)
|
||||||
super(*rest)
|
super(*rest)
|
||||||
|
|
||||||
if (token = headers["authorization"]?) && token[..Auth::BEARER.size - 1] == Auth::BEARER
|
if (token = headers["authorization"]?) && token.starts_with?(Auth::BEARER)
|
||||||
payload = Auth.decode_jwt?(token[Auth::BEARER.size..])
|
begin
|
||||||
return unless payload
|
payload = Auth::Token.decode(token[Auth::BEARER.size..].strip)
|
||||||
|
rescue ex : JWT::ExpiredSignatureError
|
||||||
data = payload["data"].as_h
|
@status = Status::SessionExpired
|
||||||
return unless @user = Db::User.find(data["user_id"].as_i)
|
rescue
|
||||||
|
@status = Status::JWTError
|
||||||
if @user
|
else
|
||||||
@admin = user.not_nil!.admin
|
if @user = Db::User.find(payload.context.user)
|
||||||
@role = user.not_nil!.role.to_api
|
@admin = user.not_nil!.admin
|
||||||
@external =
|
@role = user.not_nil!.role.to_api
|
||||||
case @role.not_nil!
|
@external =
|
||||||
when .teacher?
|
case @role.not_nil!
|
||||||
@user.not_nil!.teacher
|
when .teacher?
|
||||||
when .student?
|
@user.not_nil!.teacher
|
||||||
@user.not_nil!.student
|
when .student?
|
||||||
end
|
@user.not_nil!.student
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def on_development : Nil
|
||||||
|
{% if !flag?(:release) %}
|
||||||
|
if @development
|
||||||
|
yield
|
||||||
|
end
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
|
||||||
|
enum Status
|
||||||
|
OK
|
||||||
|
SessionExpired
|
||||||
|
JWTError
|
||||||
|
end
|
||||||
|
|
||||||
# User is authenticated
|
# User is authenticated
|
||||||
def authenticated? : Bool
|
def authenticated? : Bool
|
||||||
!!@user
|
!!@user && @status.ok?
|
||||||
end
|
end
|
||||||
|
|
||||||
# :ditto:
|
# :ditto:
|
||||||
def authenticated! : Bool
|
def authenticated! : Bool
|
||||||
|
raise "Session expired" if @status.session_expired?
|
||||||
raise "Not authenticated" unless authenticated?
|
raise "Not authenticated" unless authenticated?
|
||||||
|
|
||||||
true
|
true
|
||||||
|
@ -88,20 +111,21 @@ module Backend
|
||||||
|
|
||||||
# :ditto:
|
# :ditto:
|
||||||
def admin! : Bool
|
def admin! : Bool
|
||||||
|
authenticated!
|
||||||
raise "Invalid permissions" unless admin?
|
raise "Invalid permissions" unless admin?
|
||||||
|
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
# User's is one of *roles*
|
# User's is one of *roles*
|
||||||
def role?(external_check = true, *roles : Schema::UserRole) : Bool
|
def role?(roles : Array(Schema::UserRole), external_check = true) : Bool
|
||||||
return false unless authenticated?
|
return false unless authenticated?
|
||||||
|
|
||||||
roles.each do |role|
|
roles.each do |role|
|
||||||
return true if @role == role &&
|
return true if @role == role &&
|
||||||
if external_check
|
if external_check
|
||||||
role ==
|
role ==
|
||||||
case @external.not_nil! # TODO: Simplify with Germanium in future but for now with macro iteration over `#resolve#constants`
|
case @external.not_nil!
|
||||||
when Db::Teacher
|
when Db::Teacher
|
||||||
Schema::UserRole::Teacher
|
Schema::UserRole::Teacher
|
||||||
when Db::Student
|
when Db::Student
|
||||||
|
@ -116,33 +140,64 @@ module Backend
|
||||||
end
|
end
|
||||||
|
|
||||||
# :ditto:
|
# :ditto:
|
||||||
def role!(external_check = true, *roles : Schema::UserRole) : Bool
|
def role!(roles : Array(Schema::UserRole), external_check = true) : Bool
|
||||||
raise "Invalid permissions" unless role? external, *roles
|
authenticated!
|
||||||
|
raise "Invalid permissions" unless role?(roles, external_check)
|
||||||
|
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
# User is teacher
|
# User is teacher
|
||||||
def teacher?(external_check = true) : Bool
|
def teacher?(external_check = true) : Bool
|
||||||
role? external_check, Schema::UserRole::Teacher
|
role?([Schema::UserRole::Teacher], external_check)
|
||||||
end
|
end
|
||||||
|
|
||||||
# :ditto:
|
# :ditto:
|
||||||
def teacher!(external_check = true) : Bool
|
def teacher!(external_check = true) : Bool
|
||||||
role! external_check, Schema::UserRole::Teacher
|
role!([Schema::UserRole::Teacher], external_check)
|
||||||
end
|
end
|
||||||
|
|
||||||
# User is student
|
# User is student
|
||||||
def student?(external_check = true) : Bool
|
def student?(external_check = true) : Bool
|
||||||
role? external_check, Schema::UserRole::Student
|
role?([Schema::UserRole::Student], external_check)
|
||||||
end
|
end
|
||||||
|
|
||||||
# :ditto:
|
# :ditto:
|
||||||
def student!(external_check = true) : Bool
|
def student!(external_check = true) : Bool
|
||||||
role! external_check, Schema::UserRole::Student
|
role!([Schema::UserRole::Student], external_check)
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: Custom error handler
|
# Custom error handler
|
||||||
|
def handle_exception(ex : Exception) : String?
|
||||||
|
pp! ex, ex.message
|
||||||
|
|
||||||
|
# ex.message
|
||||||
|
|
||||||
|
case ex
|
||||||
|
when Errors::Error
|
||||||
|
{% if !flag?(:release) %}
|
||||||
|
if @development
|
||||||
|
ex.message
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
{% else %}
|
||||||
|
nil
|
||||||
|
{% end %}
|
||||||
|
when Errors::PublicError
|
||||||
|
ex.message
|
||||||
|
else
|
||||||
|
{% if !flag?(:release) %}
|
||||||
|
if @development
|
||||||
|
ex.message
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
{% else %}
|
||||||
|
nil
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
13
backend/src/backend/api/errors.cr
Normal file
13
backend/src/backend/api/errors.cr
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
module Backend::Api::Errors
|
||||||
|
abstract class Error < Exception
|
||||||
|
end
|
||||||
|
|
||||||
|
abstract class PrivateError < Error
|
||||||
|
end
|
||||||
|
|
||||||
|
abstract class PublicError < Error
|
||||||
|
end
|
||||||
|
|
||||||
|
class AuthenticationError < PublicError
|
||||||
|
end
|
||||||
|
end
|
|
@ -15,6 +15,7 @@
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
require "ldap"
|
require "ldap"
|
||||||
|
require "uuid"
|
||||||
|
|
||||||
module Backend
|
module Backend
|
||||||
module Api
|
module Api
|
||||||
|
@ -24,17 +25,21 @@ module Backend
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
# Logs in as *username* with credential *password*
|
# Logs in as *username* with credential *password*
|
||||||
def login(username : String, password : String) : LoginPayload
|
def login(username : String, password : String) : LoginPayload
|
||||||
raise "Auth failed" if username.empty? || password.empty?
|
raise Errors::AuthenticationError.new if username.empty? || password.empty?
|
||||||
|
|
||||||
user = Db::User.query.find { var(:username) == username }
|
user = Db::User.query.find { var(:username) == username }
|
||||||
raise "Auth failed" unless user && Ldap.authenticate?(Ldap::DN.uid(username), password)
|
raise Errors::AuthenticationError.new unless user && Ldap.authenticate?(Ldap::DN.uid(username), password)
|
||||||
|
|
||||||
|
jti = UUID.random
|
||||||
LoginPayload.new(
|
LoginPayload.new(
|
||||||
user: User.new(user),
|
user: User.new(user),
|
||||||
token: Auth.create_user_jwt(
|
token: Auth::Token.new(
|
||||||
user.id.not_nil!.to_i,
|
iss: "mentorenwahl",
|
||||||
(Time.utc + Backend.config.api.jwt_expiration.minutes).to_unix
|
iat: Time.utc.to_unix,
|
||||||
),
|
exp: (Time.utc + Backend.config.api.jwt_expiration.minutes).to_unix,
|
||||||
|
jti: jti.hexstring,
|
||||||
|
context: Auth::Context.new(user.id.not_nil!)
|
||||||
|
).encode
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -48,7 +53,7 @@ module Backend
|
||||||
rescue LDAP::Client::AuthError
|
rescue LDAP::Client::AuthError
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
user = Db::User.create!(username: input.username, role: input.role.to_db, skif: input.skif, admin: input.admin)
|
user = Db::User.create!(username: input.username, role: input.role.to_db, admin: input.admin)
|
||||||
Worker::Jobs::CacheLdapUserJob.new(user.id.not_nil!.to_i).enqueue
|
Worker::Jobs::CacheLdapUserJob.new(user.id.not_nil!.to_i).enqueue
|
||||||
|
|
||||||
User.new(user)
|
User.new(user)
|
||||||
|
@ -65,16 +70,6 @@ module Backend
|
||||||
id
|
id
|
||||||
end
|
end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
|
||||||
# Sends all unregistered teachers a registration email
|
|
||||||
def send_teachers_registration_email(context : Context) : Bool
|
|
||||||
context.admin!
|
|
||||||
|
|
||||||
Worker::Jobs::SendTeachersRegistrationEmailJob.new.enqueue
|
|
||||||
|
|
||||||
true
|
|
||||||
end
|
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
# Starts assignment job of mentors to students
|
# Starts assignment job of mentors to students
|
||||||
def assign_students(context : Context) : Bool
|
def assign_students(context : Context) : Bool
|
||||||
|
@ -85,68 +80,68 @@ module Backend
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
# @[GraphQL::Field]
|
||||||
# Creates teacher
|
# # Creates teacher
|
||||||
def create_teacher(context : Context, input : TeacherCreateInput) : Teacher
|
# def create_teacher(context : Context, input : TeacherCreateInput) : Teacher
|
||||||
context.admin!
|
# context.admin!
|
||||||
|
|
||||||
teacher = Db::Teacher.create!(user_id: input.user_id, max_students: input.max_students)
|
# teacher = Db::Teacher.create!(user_id: input.user_id, max_students: input.max_students)
|
||||||
Teacher.new(teacher)
|
# Teacher.new(teacher)
|
||||||
end
|
# end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
# @[GraphQL::Field]
|
||||||
# Deletes teacher by ID
|
# # Deletes teacher by ID
|
||||||
def delete_teacher(context : Context, id : Int32) : Int32
|
# def delete_teacher(context : Context, id : Int32) : Int32
|
||||||
context.admin!
|
# context.admin!
|
||||||
|
|
||||||
teacher = Db::Teacher.find!(id)
|
# teacher = Db::Teacher.find!(id)
|
||||||
teacher.delete
|
# teacher.delete
|
||||||
|
|
||||||
id
|
# id
|
||||||
end
|
# end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
# @[GraphQL::Field]
|
||||||
# Self register as teacher
|
# # Self register as teacher
|
||||||
def register_teacher(context : Context, input : TeacherInput) : Teacher
|
# def register_teacher(context : Context, input : TeacherInput) : Teacher
|
||||||
context.teacher! external_check: false
|
# context.teacher! external_check: false
|
||||||
|
|
||||||
Teacher.new(
|
# Teacher.new(
|
||||||
Db::Teacher.create!(user_id: context.user.not_nil!.id, max_students: input.max_students)
|
# Db::Teacher.create!(user_id: context.user.not_nil!.id, max_students: input.max_students)
|
||||||
)
|
# )
|
||||||
end
|
# end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
# @[GraphQL::Field]
|
||||||
# Creates student
|
# # Creates student
|
||||||
def create_student(context : Context, input : StudentCreateInput) : Student
|
# def create_student(context : Context, input : StudentCreateInput) : Student
|
||||||
context.admin!
|
# context.admin!
|
||||||
|
|
||||||
user = Db::User.find!(input.user_id)
|
# user = Db::User.find!(input.user_id)
|
||||||
raise "User not a student" unless user.role.to_api.student?
|
# raise "User not a student" unless user.role.to_api.student?
|
||||||
|
|
||||||
student = Db::Student.create!(user_id: user.id)
|
# student = Db::Student.create!(user_id: user.id)
|
||||||
Student.new(student)
|
# Student.new(student)
|
||||||
end
|
# end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
# @[GraphQL::Field]
|
||||||
# Deletes student by ID
|
# # Deletes student by ID
|
||||||
def delete_student(context : Context, id : Int32) : Int32
|
# def delete_student(context : Context, id : Int32) : Int32
|
||||||
context.admin!
|
# context.admin!
|
||||||
|
|
||||||
student = Db::Student.find!(id)
|
# student = Db::Student.find!(id)
|
||||||
student.delete
|
# student.delete
|
||||||
|
|
||||||
id
|
# id
|
||||||
end
|
# end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
# @[GraphQL::Field]
|
||||||
# Self register as student
|
# # Self register as student
|
||||||
def register_student(context : Context) : Student
|
# def register_student(context : Context) : Student
|
||||||
context.student! external_check: false
|
# context.student! external_check: false
|
||||||
|
|
||||||
Student.new(
|
# Student.new(
|
||||||
Db::Student.create!(user_id: context.user.not_nil!.id)
|
# Db::Student.create!(user_id: context.user.not_nil!.id)
|
||||||
)
|
# )
|
||||||
end
|
# end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
# Creates vote for authenticated user's student
|
# Creates vote for authenticated user's student
|
||||||
|
@ -163,12 +158,12 @@ module Backend
|
||||||
|
|
||||||
if teacher.nil?
|
if teacher.nil?
|
||||||
raise "Teachers not found"
|
raise "Teachers not found"
|
||||||
elsif teacher.user.skif != context.user.not_nil!.skif
|
# elsif teacher.user.skif != context.user.not_nil!.skif
|
||||||
if teacher.user.skif
|
# if teacher.user.skif
|
||||||
raise "Teacher is SKIF, student is not"
|
# raise "Teacher is SKIF, student is not"
|
||||||
else
|
# else
|
||||||
raise "Teacher is not SKIF, student is"
|
# raise "Teacher is not SKIF, student is"
|
||||||
end
|
# end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -25,7 +25,12 @@ module Backend
|
||||||
|
|
||||||
# DB representation of the enum
|
# DB representation of the enum
|
||||||
def to_db : Db::UserRole
|
def to_db : Db::UserRole
|
||||||
Db::UserRole.from_string(self.to_s.underscore)
|
case self
|
||||||
|
in Teacher
|
||||||
|
Db::UserRole::Teacher
|
||||||
|
in Student
|
||||||
|
Db::UserRole::Student
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# GraphQL representation of the DB enum
|
# GraphQL representation of the DB enum
|
||||||
|
@ -45,7 +50,7 @@ module Backend
|
||||||
|
|
||||||
# LDAP user data
|
# LDAP user data
|
||||||
def ldap : Ldap::User
|
def ldap : Ldap::User
|
||||||
unless raw_cache = Redis::CLIENT.get("ldap:user:#{id}")
|
if @ldap.nil? && (raw_cache = Redis::CLIENT.get("ldap:user:#{id}")).nil?
|
||||||
Worker::Jobs::CacheLdapUserJob.new(id).perform
|
Worker::Jobs::CacheLdapUserJob.new(id).perform
|
||||||
raw_cache = Redis::CLIENT.get("ldap:user:#{id}").not_nil!
|
raw_cache = Redis::CLIENT.get("ldap:user:#{id}").not_nil!
|
||||||
end
|
end
|
||||||
|
@ -67,8 +72,8 @@ module Backend
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
# User's full name
|
# User's full name
|
||||||
def name : String
|
def name(formal : Bool = true) : String
|
||||||
ldap.name
|
ldap.name(formal)
|
||||||
end
|
end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
|
@ -95,40 +100,27 @@ module Backend
|
||||||
@model.role.to_api
|
@model.role.to_api
|
||||||
end
|
end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
|
||||||
# User is at SKIF
|
|
||||||
def skif : Bool
|
|
||||||
@model.skif
|
|
||||||
end
|
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
# User's external ID
|
# User's external ID
|
||||||
def external_id : Int32?
|
def external_id : Int32
|
||||||
case @model.role.to_api
|
case @model.role.to_api
|
||||||
when .teacher?
|
when Db::UserRole::Teacher
|
||||||
@model.teacher
|
@model.teacher
|
||||||
when .student?
|
when Db::UserRole::Student
|
||||||
@model.student
|
@model.student
|
||||||
end
|
end.not_nil!.id
|
||||||
.try(&.id.try(&.to_i))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
# User's external teacher object
|
# User's external teacher object
|
||||||
def teacher : Teacher?
|
def teacher : Teacher?
|
||||||
teacher = @model.teacher
|
@model.teacher.try { |t| Teacher.new(t) }
|
||||||
if teacher
|
|
||||||
Teacher.new(teacher)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
# User's external student object
|
# User's external student object
|
||||||
def student : Student?
|
def student : Student?
|
||||||
student = @model.student
|
@model.student.try { |s| Student.new(s) }
|
||||||
if student
|
|
||||||
Student.new(student)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -137,14 +129,12 @@ module Backend
|
||||||
class UserCreateInput < GraphQL::BaseInputObject
|
class UserCreateInput < GraphQL::BaseInputObject
|
||||||
getter username
|
getter username
|
||||||
getter role
|
getter role
|
||||||
getter skif
|
|
||||||
getter admin
|
getter admin
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
def initialize(
|
def initialize(
|
||||||
@username : String,
|
@username : String,
|
||||||
@role : UserRole,
|
@role : UserRole,
|
||||||
@skif : Bool,
|
|
||||||
@admin : Bool = false
|
@admin : Bool = false
|
||||||
)
|
)
|
||||||
end
|
end
|
|
@ -1,5 +1,4 @@
|
||||||
require "commander"
|
require "commander"
|
||||||
require "compiled_license"
|
|
||||||
|
|
||||||
module Backend
|
module Backend
|
||||||
CLI = Commander::Command.new do |cmd|
|
CLI = Commander::Command.new do |cmd|
|
||||||
|
@ -36,7 +35,7 @@ module Backend
|
||||||
c.long = c.short
|
c.long = c.short
|
||||||
|
|
||||||
c.run do
|
c.run do
|
||||||
puts CompiledLicense::LICENSES
|
puts LICENSES
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -55,13 +54,6 @@ module Backend
|
||||||
c.short = "Seeds the database with required data"
|
c.short = "Seeds the database with required data"
|
||||||
c.long = c.short
|
c.long = c.short
|
||||||
|
|
||||||
c.flags.add do |f|
|
|
||||||
f.name = "skif"
|
|
||||||
f.long = "--skif"
|
|
||||||
f.default = false
|
|
||||||
f.description = "User at SKIF"
|
|
||||||
end
|
|
||||||
|
|
||||||
c.flags.add do |f|
|
c.flags.add do |f|
|
||||||
f.name = "admin"
|
f.name = "admin"
|
||||||
f.long = "--admin"
|
f.long = "--admin"
|
||||||
|
@ -85,7 +77,14 @@ module Backend
|
||||||
abort unless gets(chomp: true).not_nil!.strip.downcase == "y"
|
abort unless gets(chomp: true).not_nil!.strip.downcase == "y"
|
||||||
end
|
end
|
||||||
|
|
||||||
user = Db::User.create!(username: username, role: role.to_s, skif: opts.bool["skif"], admin: opts.bool["admin"])
|
user = Db::User.create!(username: username, role: role.to_s, admin: opts.bool["admin"])
|
||||||
|
case role.to_api
|
||||||
|
in Api::Schema::UserRole::Student
|
||||||
|
Db::Student.create!(user_id: user.id)
|
||||||
|
in Api::Schema::UserRole::Teacher
|
||||||
|
Db::Teacher.create!(user_id: user.id)
|
||||||
|
end
|
||||||
|
|
||||||
Worker::Jobs::CacheLdapUserJob.new(user.id).enqueue
|
Worker::Jobs::CacheLdapUserJob.new(user.id).enqueue
|
||||||
|
|
||||||
puts "Done!"
|
puts "Done!"
|
|
@ -32,25 +32,25 @@ module Backend
|
||||||
|
|
||||||
# Types of environments program can compiled for / with
|
# Types of environments program can compiled for / with
|
||||||
enum BuildEnv
|
enum BuildEnv
|
||||||
Production
|
Release
|
||||||
Development
|
Development
|
||||||
end
|
end
|
||||||
|
|
||||||
# Type of environment program is running in
|
# Type of environment program is running in
|
||||||
def build_env : BuildEnv
|
def build_env : BuildEnv
|
||||||
{{ flag?(:development) }} ? BuildEnv::Development : BuildEnv::Production
|
{{ flag?(:release) }} ? BuildEnv::Release : BuildEnv::Development
|
||||||
end
|
end
|
||||||
|
|
||||||
# Production mode
|
# Release mode
|
||||||
#
|
#
|
||||||
# `true` if the build environment is `BuildEnv::Development`
|
# `true` if the build environment is `BuildEnv::Release`
|
||||||
def production? : Bool
|
def release? : Bool
|
||||||
build_env.production?
|
build_env.production?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Development mode
|
# Development mode
|
||||||
#
|
#
|
||||||
# `true` if the build environment is `BuildEnv::Production`
|
# `true` if the build environment is `BuildEnv::Development`
|
||||||
def development? : Bool
|
def development? : Bool
|
||||||
build_env.development?
|
build_env.development?
|
||||||
end
|
end
|
||||||
|
@ -87,20 +87,15 @@ module Backend
|
||||||
class ApiConfig
|
class ApiConfig
|
||||||
include EnvConfig
|
include EnvConfig
|
||||||
|
|
||||||
# GraphQL playground enable
|
|
||||||
getter graphql_playground : Bool
|
|
||||||
|
|
||||||
# JWT signing key
|
# JWT signing key
|
||||||
getter jwt_secret : String
|
getter jwt_secret : String
|
||||||
|
|
||||||
# JWT expiration time in minutes
|
# JWT expiration time in minutes
|
||||||
getter jwt_expiration : Int32
|
getter jwt_expiration : Int32
|
||||||
|
|
||||||
# Helper method for enabling GraphQL playground
|
# Returns true of `playground` flag was set on compile time
|
||||||
#
|
def graphql_playground? : Bool
|
||||||
# Returns `true` if `Config#development?` or `#graphql_playground` are
|
flag?(:playground)
|
||||||
def graphql_playground_fully_enabled? : Bool
|
|
||||||
Backend.config.development? || graphql_playground
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,7 +29,7 @@ module Backend
|
||||||
# Migration UIDs
|
# Migration UIDs
|
||||||
MIGRATIONS = {{ run("./macros/migrations.cr", "db/migrations/*.sql").stringify.split("\n") }}
|
MIGRATIONS = {{ run("./macros/migrations.cr", "db/migrations/*.sql").stringify.split("\n") }}
|
||||||
|
|
||||||
def init(severity = {% if flag?(:development) %} ::Log::Severity::Debug {% else %} ::Log::Severity::Info {% end %}) : Nil
|
def init(severity = {% if !flag?(:release) %} ::Log::Severity::Debug {% else %} ::Log::Severity::Info {% end %}) : Nil
|
||||||
::Log.builder.bind "clear.*", severity, ::Log::IOBackend.new
|
::Log.builder.bind "clear.*", severity, ::Log::IOBackend.new
|
||||||
Retriable.retry(on: DB::ConnectionRefused, backoff: false) do
|
Retriable.retry(on: DB::ConnectionRefused, backoff: false) do
|
||||||
Clear::SQL.init(Backend.config.db.url)
|
Clear::SQL.init(Backend.config.db.url)
|
|
@ -5,7 +5,14 @@ module Backend
|
||||||
struct UserRole
|
struct UserRole
|
||||||
# API representation of the enum
|
# API representation of the enum
|
||||||
def to_api : Api::Schema::UserRole
|
def to_api : Api::Schema::UserRole
|
||||||
Api::Schema::UserRole.parse(self.to_s)
|
case self
|
||||||
|
when Student
|
||||||
|
Api::Schema::UserRole::Student
|
||||||
|
when Teacher
|
||||||
|
Api::Schema::UserRole::Teacher
|
||||||
|
else
|
||||||
|
raise "Invalid enum value for UserRole"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# DB representation of the enum
|
# DB representation of the enum
|
||||||
|
@ -23,7 +30,7 @@ module Backend
|
||||||
column username : String
|
column username : String
|
||||||
column role : UserRole
|
column role : UserRole
|
||||||
column admin : Bool = false
|
column admin : Bool = false
|
||||||
column skif : Bool
|
column jti : UUID?
|
||||||
|
|
||||||
has_one student : Student?, foreign_key: :user_id
|
has_one student : Student?, foreign_key: :user_id
|
||||||
has_one teacher : Teacher?, foreign_key: :user_id
|
has_one teacher : Teacher?, foreign_key: :user_id
|
|
@ -26,22 +26,26 @@ module Backend
|
||||||
|
|
||||||
@[JSON::Field(key: "givenName")]
|
@[JSON::Field(key: "givenName")]
|
||||||
# First name
|
# First name
|
||||||
property first_name : String
|
getter first_name : String
|
||||||
|
|
||||||
@[JSON::Field(key: "sn")]
|
@[JSON::Field(key: "sn")]
|
||||||
# Last name
|
# Last name
|
||||||
property last_name : String
|
getter last_name : String
|
||||||
|
|
||||||
@[JSON::Field(key: "mail")]
|
@[JSON::Field(key: "mail")]
|
||||||
# Email address
|
# Email address
|
||||||
property email : String
|
getter email : String
|
||||||
|
|
||||||
def initialize(@first_name : String, @last_name : String, @email : String)
|
def initialize(@first_name : String, @last_name : String, @email : String)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Name
|
# Name
|
||||||
def name : String
|
def name(formal = true) : String
|
||||||
"#{first_name} #{last_name}"
|
if formal
|
||||||
|
"#{@last_name}, #{@first_name}"
|
||||||
|
else
|
||||||
|
"#{@first_name} #{@last_name}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Creates user data from LDAP entry
|
# Creates user data from LDAP entry
|
5
backend/src/backend/licenses.cr
Normal file
5
backend/src/backend/licenses.cr
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
require "compiled_license"
|
||||||
|
|
||||||
|
module Backend
|
||||||
|
LICENSES = CompiledLicense::LICENSES
|
||||||
|
end
|
|
@ -36,7 +36,7 @@ module Backend
|
||||||
|
|
||||||
# Run the backend
|
# Run the backend
|
||||||
def run : self
|
def run : self
|
||||||
{% if flag?(:development) %}
|
{% if !flag?(:release) %}
|
||||||
Log.warn { "Backend is running in development mode! Do not use this in production!" }
|
Log.warn { "Backend is running in development mode! Do not use this in production!" }
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
require "http/headers"
|
require "http/headers"
|
||||||
require "mime"
|
require "baked_file_system"
|
||||||
|
|
||||||
module Backend
|
module Backend
|
||||||
module Web
|
module Web
|
||||||
|
@ -23,25 +23,29 @@ module Backend
|
||||||
@[ARTA::Route(path: "/")]
|
@[ARTA::Route(path: "/")]
|
||||||
# GraphQL API controller
|
# GraphQL API controller
|
||||||
class ApiController < ATH::Controller
|
class ApiController < ATH::Controller
|
||||||
@[ARTA::Get("")]
|
{% if flag?(:playground) %}
|
||||||
def playground : ATH::Response | ATH::Exceptions::HTTPException
|
# Public folder virtual filesystem
|
||||||
{% if flag?(:development) %}
|
private module Public
|
||||||
ATH::StreamedResponse.new(headers: HTTP::Headers{"content-type" => MIME.from_extension(".html")}) do |io|
|
extend BakedFileSystem
|
||||||
|
|
||||||
|
bake_folder "../../../../public"
|
||||||
|
end
|
||||||
|
|
||||||
|
@[ARTA::Get("")]
|
||||||
|
def playground : ATH::Response
|
||||||
|
ATH::StreamedResponse.new(headers: HTTP::Headers{"Content-Type" => "text/html"}) do |io|
|
||||||
IO.copy(Public.get("index.html"), io)
|
IO.copy(Public.get("index.html"), io)
|
||||||
end
|
end
|
||||||
{% else %}
|
end
|
||||||
if Backend.config.api.graphql_playground_fully_enabled?
|
{% else %}
|
||||||
ATH::StreamedResponse.new(headers: HTTP::Headers{"content-type" => MIME.from_extension(".html")}) do |io|
|
@[ARTA::Get("")]
|
||||||
IO.copy(Public.get("index.html"), io)
|
def playground : ATH::Exceptions::ServiceUnavailable
|
||||||
end
|
ATH::Exceptions::ServiceUnavailable.new("GraphQL playground is disabled")
|
||||||
else
|
end
|
||||||
ATH::Exceptions::ServiceUnavailable.new("GraphQL Playground is not enabled. Please enable it in the backend configuration.")
|
{% end %}
|
||||||
end
|
|
||||||
{% end %}
|
|
||||||
end
|
|
||||||
|
|
||||||
# GraphQL query request data
|
# GraphQL query data
|
||||||
struct GraphQLQueryData
|
struct GraphQLQuery
|
||||||
include JSON::Serializable
|
include JSON::Serializable
|
||||||
|
|
||||||
# Raw query
|
# Raw query
|
||||||
|
@ -55,15 +59,18 @@ module Backend
|
||||||
end
|
end
|
||||||
|
|
||||||
@[ARTA::Post("")]
|
@[ARTA::Post("")]
|
||||||
@[ATHA::QueryParam("development")]
|
{% if !flag?(:release) %}
|
||||||
def endpoint(request : ATH::Request, development : Bool = false) : ATH::Response
|
@[ATHA::QueryParam("development", description: "Enables development mode")]
|
||||||
{% if flag?(:development) %}
|
{% end %}
|
||||||
|
def endpoint(request : ATH::Request, development : Bool = false) : ATH::Exceptions::BadRequest | ATH::Response
|
||||||
|
{% if !flag?(:release) %}
|
||||||
Log.notice { "Development request icoming" } if development
|
Log.notice { "Development request icoming" } if development
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
||||||
query = GraphQLQueryData.from_json(request.body.not_nil!)
|
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" => MIME.from_extension(".json")}) do |io|
|
ATH::StreamedResponse.new(headers: HTTP::Headers{"content-type" => "application/json"}) do |io|
|
||||||
Api::Schema::SCHEMA.execute(
|
Api::Schema::SCHEMA.execute(
|
||||||
io,
|
io,
|
||||||
query.query,
|
query.query,
|
|
@ -21,23 +21,23 @@ require "./worker/*"
|
||||||
module Backend
|
module Backend
|
||||||
# Worker module
|
# Worker module
|
||||||
module Worker
|
module Worker
|
||||||
# :inherit:
|
# # :inherit:
|
||||||
module Mosquito::Serializers::Granite
|
# module Mosquito::Serializers::Granite
|
||||||
# :inherit:
|
# # :inherit:
|
||||||
macro serialize_granite_model(klass)
|
# macro serialize_granite_model(klass)
|
||||||
{% method_suffix = klass.resolve.stringify.underscore.gsub(/::/, "__").id %}
|
# {% method_suffix = klass.resolve.stringify.underscore.gsub(/::/, "__").id %}
|
||||||
|
|
||||||
# Serializes {{ klaas.id }} to redis manageable data
|
# # Serializes {{ klaas.id }} to redis manageable data
|
||||||
def serialize_{{ method_suffix }}(model : {{ klass.id }}) : String
|
# def serialize_{{ method_suffix }}(model : {{ klass.id }}) : String
|
||||||
model.id.to_s
|
# model.id.to_s
|
||||||
end
|
# end
|
||||||
|
|
||||||
# Deserializes {{ klaas.id }} from redis manageable data
|
# # Deserializes {{ klaas.id }} from redis manageable data
|
||||||
def deserialize_{{ method_suffix }}(raw : String) : {{ klass.id }}
|
# def deserialize_{{ method_suffix }}(raw : String) : {{ klass.id }}
|
||||||
{{ klass.id }}.find!(raw.to_i)
|
# {{ klass.id }}.find!(raw.to_i)
|
||||||
end
|
# end
|
||||||
end
|
# end
|
||||||
end
|
# end
|
||||||
|
|
||||||
Mosquito.configure do |settings|
|
Mosquito.configure do |settings|
|
||||||
settings.redis_url = Backend.config.redis.url
|
settings.redis_url = Backend.config.redis.url
|
|
@ -72,10 +72,10 @@ module Backend
|
||||||
|
|
||||||
# pp! possibilities
|
# pp! possibilities
|
||||||
|
|
||||||
teacher_ids = Db::Teacher.query
|
# teacher_ids = Db::Teacher.query
|
||||||
.select("id")
|
# .select("id")
|
||||||
.where { raw("(SELECT COUNT(*) FROM assignments WHERE teacher_id = teachers.id)") < max_students }
|
# .where { raw("(SELECT COUNT(*) FROM assignments WHERE teacher_id = teachers.id)") < max_students }
|
||||||
.map(&.id)
|
# .map(&.id)
|
||||||
students = Db::Student.query
|
students = Db::Student.query
|
||||||
.where do
|
.where do
|
||||||
raw("NOT EXISTS (SELECT 1 FROM assignments WHERE student_id = students.id)") &
|
raw("NOT EXISTS (SELECT 1 FROM assignments WHERE student_id = students.id)") &
|
||||||
|
@ -100,13 +100,12 @@ module Backend
|
||||||
end
|
end
|
||||||
|
|
||||||
assignments = [] of {assignment: Hash(Int32, Array(Int32)), weighting: Int32}
|
assignments = [] of {assignment: Hash(Int32, Array(Int32)), weighting: Int32}
|
||||||
empty_assignment = Hash.zip(teacher_ids, [[] of {assignment: Hash(Int32, Array(Int32)), weighting: Int32}] * teacher_ids.size)
|
# empty_assignment = Hash.zip(teacher_ids, [[] of {assignment: Hash(Int32, Array(Int32)), weighting: Int32}] * teacher_ids.size)
|
||||||
Backend.config.assignment_possibility_count.times do
|
Backend.config.assignment_possibility_count.times do
|
||||||
random_votes.shuffle!(Random::Secure)
|
random_votes.shuffle!(Random::Secure)
|
||||||
|
|
||||||
votes = random_votes.dup
|
# votes = random_votes.dup
|
||||||
a = empty_assignment.clone
|
# a = empty_assignment.clone
|
||||||
|
|
||||||
end
|
end
|
||||||
pp! assignments
|
pp! assignments
|
||||||
end
|
end
|
|
@ -19,7 +19,6 @@ version: "3"
|
||||||
services:
|
services:
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:alpine
|
image: nginx:alpine
|
||||||
container_name: nginx
|
|
||||||
restart: always
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- ./config/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
- ./config/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
@ -32,7 +31,6 @@ services:
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:alpine
|
image: postgres:alpine
|
||||||
container_name: db
|
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
- db
|
- db
|
||||||
|
@ -44,7 +42,6 @@ services:
|
||||||
|
|
||||||
adminer:
|
adminer:
|
||||||
image: adminer:standalone
|
image: adminer:standalone
|
||||||
container_name: adminer
|
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
- default
|
- default
|
||||||
|
@ -54,7 +51,6 @@ services:
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:alpine
|
image: redis:alpine
|
||||||
container_name: redis
|
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
- redis
|
- redis
|
||||||
|
@ -62,11 +58,11 @@ services:
|
||||||
- redis:/data
|
- redis:/data
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
|
image: mentorenwahl/backend
|
||||||
build:
|
build:
|
||||||
context: ./docker/backend
|
context: ./backend
|
||||||
args:
|
args:
|
||||||
BUILD_ENV: production
|
BUILD_ENV: production
|
||||||
container_name: backend
|
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
- default
|
- default
|
||||||
|
@ -79,7 +75,6 @@ services:
|
||||||
BACKEND_URL: ${URL}
|
BACKEND_URL: ${URL}
|
||||||
BACKEND_MINIMUM_TEACHER_SELECTION_COUNT: ${BACKEND_MINIMUM_TEACHER_SELECTION_COUNT}
|
BACKEND_MINIMUM_TEACHER_SELECTION_COUNT: ${BACKEND_MINIMUM_TEACHER_SELECTION_COUNT}
|
||||||
BACKEND_ASSIGNMENT_POSSIBILITY_COUNT: ${BACKEND_ASSIGNMENT_POSSIBILITY_COUNT}
|
BACKEND_ASSIGNMENT_POSSIBILITY_COUNT: ${BACKEND_ASSIGNMENT_POSSIBILITY_COUNT}
|
||||||
BACKEND_API_GRAPHQL_PLAYGROUND: ${BACKEND_API_GRAPHQL_PLAYGROUND}
|
|
||||||
BACKEND_API_JWT_SECRET: ${BACKEND_API_JWT_SECRET}
|
BACKEND_API_JWT_SECRET: ${BACKEND_API_JWT_SECRET}
|
||||||
BACKEND_API_JWT_EXPIRATION: ${BACKEND_API_JWT_EXPIRATION}
|
BACKEND_API_JWT_EXPIRATION: ${BACKEND_API_JWT_EXPIRATION}
|
||||||
BACKEND_SMTP_HELO: ${BACKEND_SMTP_HELO}
|
BACKEND_SMTP_HELO: ${BACKEND_SMTP_HELO}
|
||||||
|
@ -101,9 +96,9 @@ services:
|
||||||
BACKEND_LDAP_CACHE_REFRESH_INTERVAL: ${BACKEND_LDAP_CACHE_REFRESH_INTERVAL}
|
BACKEND_LDAP_CACHE_REFRESH_INTERVAL: ${BACKEND_LDAP_CACHE_REFRESH_INTERVAL}
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
|
image: mentorenwahl/frontend
|
||||||
build:
|
build:
|
||||||
context: ./docker/frontend
|
context: ./frontend
|
||||||
container_name: frontend
|
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
- default
|
- default
|
||||||
|
@ -116,6 +111,7 @@ networks:
|
||||||
db:
|
db:
|
||||||
redis:
|
redis:
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
db:
|
db:
|
||||||
redis:
|
redis:
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
require "jwt"
|
|
||||||
|
|
||||||
module Backend
|
|
||||||
module Api
|
|
||||||
# Authorization and authentication utilities
|
|
||||||
module Auth
|
|
||||||
extend self
|
|
||||||
|
|
||||||
# Bearer token header
|
|
||||||
BEARER = "Bearer "
|
|
||||||
|
|
||||||
# Creates raw JWT token
|
|
||||||
#
|
|
||||||
# WARNING: Always use a wrapper for this method
|
|
||||||
private def create_jwt(data, expiration : Int) : String
|
|
||||||
JWT.encode({"data" => data.to_h, "exp" => expiration}, Backend.config.api.jwt_secret, JWT::Algorithm::HS256)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Decodes JWT token
|
|
||||||
def decode_jwt(jwt : String) : JSON::Any
|
|
||||||
JWT.decode(jwt, Backend.config.api.jwt_secret, JWT::Algorithm::HS256)[0]
|
|
||||||
end
|
|
||||||
|
|
||||||
# :ditto:
|
|
||||||
def decode_jwt?(jwt : String) : JSON::Any?
|
|
||||||
decode_jwt(jwt)
|
|
||||||
rescue
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
# Creates JWT token for user
|
|
||||||
def create_user_jwt(user_id : Int, expiration : Int) : String
|
|
||||||
create_jwt({user_id: user_id}, expiration)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,32 +0,0 @@
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
module Backend
|
|
||||||
module Mailers
|
|
||||||
# Sends teacher a polite registration mail to ask if they may input their data
|
|
||||||
class TeacherRegistrationMailer < Quartz::Composer
|
|
||||||
def sender : Quartz::Message::Address
|
|
||||||
address email: Backend.config.smtp.username, name: Backend.config.smtp.name
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize(user : Ldap::User)
|
|
||||||
to name: user.name, email: user.email
|
|
||||||
subject "Mentorenwahl Lehrer Registrierung"
|
|
||||||
text Kilt.render("#{__DIR__}/templates/teacher_registration_mailer.txt.ecr")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,5 +0,0 @@
|
||||||
Hey, <%= user.first_name %>!
|
|
||||||
|
|
||||||
Du wurdest erfolgreich als Lehrer registriert.
|
|
||||||
Initialisiere deinen Account, indem du auf den folgenden Link klickst und deine Daten eingibst:
|
|
||||||
<%= Path[Backend.config.url, "login"] %>
|
|
|
@ -1,26 +0,0 @@
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
require "baked_file_system"
|
|
||||||
|
|
||||||
module Backend
|
|
||||||
# Public folder virtual filesystem
|
|
||||||
module Public
|
|
||||||
extend BakedFileSystem
|
|
||||||
|
|
||||||
bake_folder "../../public"
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,49 +0,0 @@
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
module Backend
|
|
||||||
module Worker
|
|
||||||
module Jobs
|
|
||||||
# Sends all unregistered teachers a polite registration mail to ask if they may input their data
|
|
||||||
class SendTeachersRegistrationEmailJob < Mosquito::QueuedJob
|
|
||||||
# :ditto:
|
|
||||||
def perform : Nil
|
|
||||||
users = Db::User.query.where { (x.role == Db::UserRole::Teacher) & (x.teacher_id == nil) }
|
|
||||||
count = users.count.to_i
|
|
||||||
|
|
||||||
channel = Channel(Nil).new(count)
|
|
||||||
|
|
||||||
users.each do |user|
|
|
||||||
spawn do
|
|
||||||
next unless user.role.to_api.teacher?
|
|
||||||
|
|
||||||
ldap_user = Ldap::User.from_username(user.username)
|
|
||||||
log "Sending teacher registration email to #{ldap_user.email} ##{user.id}"
|
|
||||||
Mailers::TeacherRegistrationMailer.new(ldap_user).deliver
|
|
||||||
|
|
||||||
channel.send(nil)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
count.times do
|
|
||||||
channel.receive
|
|
||||||
end
|
|
||||||
Fiber.yield
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,117 +0,0 @@
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
require "commander"
|
|
||||||
require "compiled_license"
|
|
||||||
require "docker"
|
|
||||||
|
|
||||||
require "../backend"
|
|
||||||
|
|
||||||
Docker.setup
|
|
||||||
Backend::Db.init
|
|
||||||
|
|
||||||
cli = Commander::Command.new do |cmd|
|
|
||||||
cmd.use = "backend"
|
|
||||||
cmd.short = "Mentorenwahl backend CLI"
|
|
||||||
|
|
||||||
cmd.run do
|
|
||||||
puts cmd.help
|
|
||||||
end
|
|
||||||
|
|
||||||
cmd.commands.add do |c|
|
|
||||||
c.use = "version"
|
|
||||||
c.short = "Prints version"
|
|
||||||
c.long = c.short
|
|
||||||
|
|
||||||
c.run do
|
|
||||||
puts Backend::VERSION
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
cmd.commands.add do |c|
|
|
||||||
c.use = "authors"
|
|
||||||
c.short = "Prints authors"
|
|
||||||
c.long = c.short
|
|
||||||
|
|
||||||
c.run do
|
|
||||||
puts Backend::AUTHORS.join("\n")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
cmd.commands.add do |c|
|
|
||||||
c.use = "licenses"
|
|
||||||
c.short = "Prints licenses of projects used by this programs"
|
|
||||||
c.long = c.short
|
|
||||||
|
|
||||||
c.run do
|
|
||||||
puts CompiledLicense::LICENSES
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
cmd.commands.add do |c|
|
|
||||||
c.use = "run"
|
|
||||||
c.short = "Run the backend"
|
|
||||||
c.long = c.short
|
|
||||||
|
|
||||||
c.run do
|
|
||||||
Backend::Runner.new.run
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
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 = "skif"
|
|
||||||
f.long = "--skif"
|
|
||||||
f.default = false
|
|
||||||
f.description = "User at SKIF"
|
|
||||||
end
|
|
||||||
|
|
||||||
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 = Backend::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 = Backend::Db::User.create!(username: username, role: role.to_s, skif: opts.bool["skif"], admin: opts.bool["admin"])
|
|
||||||
Backend::Worker::Jobs::CacheLdapUserJob.new(user.id).enqueue
|
|
||||||
|
|
||||||
puts "Done!"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Commander.run(cli, ARGV)
|
|
|
@ -1 +0,0 @@
|
||||||
16.14.2
|
|
1
frontend/.nvmrc
Normal file
1
frontend/.nvmrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
v16.17.1
|
|
@ -1,5 +1,5 @@
|
||||||
FROM node:16-alpine
|
FROM node:16-alpine
|
||||||
WORKDIR /app
|
WORKDIR /usr/src/frontend
|
||||||
COPY ./package.json ./yarn.lock ./
|
COPY ./package.json ./yarn.lock ./
|
||||||
RUN yarn install --frozen-lockfile
|
RUN yarn install --frozen-lockfile
|
||||||
COPY . .
|
COPY . .
|
|
@ -4,10 +4,6 @@ export interface Node {
|
||||||
id: ID;
|
id: ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Skif {
|
|
||||||
skif: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum UserRole {
|
export enum UserRole {
|
||||||
STUDENT = "Student",
|
STUDENT = "Student",
|
||||||
TEACHER = "Teacher",
|
TEACHER = "Teacher",
|
||||||
|
@ -30,11 +26,11 @@ export interface UserExternal {
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Student extends Node, UserExternal, Skif {
|
export interface Student extends Node, UserExternal {
|
||||||
vote?: Vote;
|
vote?: Vote;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Teacher extends Node, UserExternal, Skif {
|
export interface Teacher extends Node, UserExternal {
|
||||||
maxStudents: number;
|
maxStudents: number;
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,81 +47,63 @@
|
||||||
`);
|
`);
|
||||||
query(meStore);
|
query(meStore);
|
||||||
|
|
||||||
const teacherRegisterFormMaxStudents = svelteForms.field("maxStudents", 0, [
|
// const teacherRegisterFormMaxStudents = svelteForms.field("maxStudents", 0, [
|
||||||
validators.required(),
|
// validators.required(),
|
||||||
validators.min(0),
|
// validators.min(0),
|
||||||
]);
|
// ]);
|
||||||
const teacherRegisterFormSkif = svelteForms.field("skif", false);
|
// const teacheRegisterForm = svelteForms.form(teacherRegisterFormMaxStudents);
|
||||||
const teacheRegisterForm = svelteForms.form(
|
|
||||||
teacherRegisterFormMaxStudents,
|
|
||||||
teacherRegisterFormSkif
|
|
||||||
);
|
|
||||||
|
|
||||||
interface RegisterTeacherData {
|
// interface RegisterTeacherData {
|
||||||
registerTeacher: Teacher;
|
// registerTeacher: Teacher;
|
||||||
}
|
// }
|
||||||
|
|
||||||
interface RegisterTeacherVars {
|
// interface RegisterTeacherVars {
|
||||||
maxStudents: number;
|
// maxStudents: number;
|
||||||
skif: boolean;
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
const registerTeacherStore = operationStore<
|
// const registerTeacherStore = operationStore<
|
||||||
RegisterTeacherData,
|
// RegisterTeacherData,
|
||||||
RegisterTeacherVars
|
// RegisterTeacherVars
|
||||||
>(gql`
|
// >(gql`
|
||||||
mutation RegisterTeacher($maxStudents: Int!, $skif: Boolean!) {
|
// mutation RegisterTeacher($maxStudents: Int!) {
|
||||||
registerTeacher(input: { maxStudents: $maxStudents, skif: $skif }) {
|
// registerTeacher(input: { maxStudents: $maxStudents }) {
|
||||||
id
|
// id
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
`);
|
// `);
|
||||||
|
|
||||||
const registerTeacherMutation = mutation(registerTeacherStore);
|
// const registerTeacherMutation = mutation(registerTeacherStore);
|
||||||
|
|
||||||
async function registerTeacher(): Promise<void> {
|
// async function registerTeacher(): Promise<void> {
|
||||||
await registerTeacherMutation({
|
// await registerTeacherMutation({
|
||||||
maxStudents: $teacherRegisterFormMaxStudents.value,
|
// maxStudents: $teacherRegisterFormMaxStudents.value,
|
||||||
skif: $teacherRegisterFormSkif.value,
|
// });
|
||||||
});
|
|
||||||
|
|
||||||
if (!$registerTeacherStore.error && $registerTeacherStore.data) {
|
// if (!$registerTeacherStore.error && $registerTeacherStore.data) {
|
||||||
location.reload();
|
// location.reload();
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
const registerStudentSkif = svelteForms.field("skif", false);
|
// interface RegisterStudentData {
|
||||||
const registerStudentForm = svelteForms.form(registerStudentSkif);
|
// registerStudent: Student;
|
||||||
|
// }
|
||||||
|
|
||||||
interface RegisterStudentData {
|
// const registerStudentStore = operationStore<RegisterStudentData>(gql`
|
||||||
registerStudent: Student;
|
// mutation RegisterStudent() {
|
||||||
}
|
// registerStudent() {
|
||||||
|
// id
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// `);
|
||||||
|
// const registerStudentMutation = mutation(registerStudentStore);
|
||||||
|
|
||||||
interface RegisterStudentVars {
|
// async function registerStudent(): Promise<void> {
|
||||||
skif: boolean;
|
// await registerStudentMutation();
|
||||||
}
|
|
||||||
|
|
||||||
const registerStudentStore = operationStore<
|
// if (!$registerStudentStore.error && $registerStudentStore.data) {
|
||||||
RegisterStudentData,
|
// location.reload();
|
||||||
RegisterStudentVars
|
// }
|
||||||
>(gql`
|
// }
|
||||||
mutation RegisterStudent($skif: Boolean!) {
|
|
||||||
registerStudent(input: { skif: $skif }) {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
const registerStudentMutation = mutation(registerStudentStore);
|
|
||||||
|
|
||||||
async function registerStudent(): Promise<void> {
|
|
||||||
await registerStudentMutation({
|
|
||||||
skif: $registerStudentSkif.value,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!$registerStudentStore.error && $registerStudentStore.data) {
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $meStore.error}
|
{#if $meStore.error}
|
||||||
|
@ -133,7 +115,7 @@
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
{#if $meStore.data.me.role === UserRole.TEACHER}
|
{#if $meStore.data.me.role === UserRole.TEACHER}
|
||||||
{#if !$meStore.data.me.teacher}
|
<!-- {#if !$meStore.data.me.teacher}
|
||||||
<p>Registriere dich jetzt als Lehrer:</p>
|
<p>Registriere dich jetzt als Lehrer:</p>
|
||||||
<form on:submit|preventDefault={registerTeacher}>
|
<form on:submit|preventDefault={registerTeacher}>
|
||||||
<label for="maxStudents">Maximale Schüler:</label>
|
<label for="maxStudents">Maximale Schüler:</label>
|
||||||
|
@ -166,9 +148,9 @@
|
||||||
{:else if $registerTeacherStore.data}
|
{:else if $registerTeacherStore.data}
|
||||||
<p>Registrierung erfolgreich!</p>
|
<p>Registrierung erfolgreich!</p>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if} -->
|
||||||
{:else if $meStore.data.me.role === UserRole.STUDENT}
|
{:else if $meStore.data.me.role === UserRole.STUDENT}
|
||||||
{#if !$meStore.data.me.student}
|
<!-- {#if !$meStore.data.me.student}
|
||||||
<p>Registriere dich jetzt als Schüler:</p>
|
<p>Registriere dich jetzt als Schüler:</p>
|
||||||
<form on:submit|preventDefault={registerStudent}>
|
<form on:submit|preventDefault={registerStudent}>
|
||||||
<label for="skif">SKIF:</label>
|
<label for="skif">SKIF:</label>
|
||||||
|
@ -192,6 +174,6 @@
|
||||||
{:else if $registerStudentStore.data}
|
{:else if $registerStudentStore.data}
|
||||||
<p>Registrierung erfolgreich!</p>
|
<p>Registrierung erfolgreich!</p>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if} -->
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
Loading…
Reference in a new issue