diff --git a/.drone.yml b/.drone.yml index 57d162d..d017d6c 100644 --- a/.drone.yml +++ b/.drone.yml @@ -43,25 +43,25 @@ steps: - name: ameba image: veelenga/ameba commands: - - cd docker/backend + - cd backend - ameba micrate/src src - name: deps - image: crystallang/crystal:1.3-alpine + image: crystallang/crystal:1.6-alpine volumes: - name: lib - path: /drone/src/docker/backend/lib + path: /drone/src/backend/lib - name: cache path: /cache commands: - - cd docker/backend + - cd backend - shards install - name: docs - image: crystallang/crystal:1.3-alpine + image: crystallang/crystal:1.6-alpine volumes: - name: lib - path: /drone/src/docker/backend/lib + path: /drone/src/backend/lib commands: - - cd docker/backend + - cd backend - make docs depends_on: - ameba @@ -74,7 +74,7 @@ steps: settings: rebuild: true mount: - - ./docker/backend/docs + - ./backend/docs depends_on: - docs - name: build @@ -83,20 +83,10 @@ steps: - name: dockersock path: /var/run/docker.sock commands: - - docker-compose build --build-arg BUILD_ENV=development backend + - docker-compose build backend depends_on: - ameba -volumes: - - name: lib - temp: {} - - name: cache - host: - path: /tmp/cache - - name: dockersock - host: - path: /var/run/docker.sock - --- kind: pipeline type: docker @@ -109,7 +99,7 @@ steps: - name: dockersock path: /var/run/docker.sock commands: - - docker-compose build --build-arg BUILD_ENV=development frontend + - docker-compose build frontend volumes: - name: dockersock @@ -131,7 +121,7 @@ steps: restore: true mount: - ./docs/book - - ./docker/backend/docs + - ./backend/docs - name: prepare-pages image: bitnami/git volumes: @@ -144,7 +134,7 @@ steps: - rm -rf /tmp/pages/* - cp -r ./docs/book/* /tmp/pages - 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: - restore-cache - name: deploy-pages diff --git a/.example.env b/.example.env index 97f61dc..e17b7bf 100644 --- a/.example.env +++ b/.example.env @@ -26,7 +26,6 @@ BACKEND_MINIMUM_TEACHER_SELECTION_COUNT=6 BACKEND_ASSIGNMENT_POSSIBILITY_COUNT=500 BACKEND_URL=URL # Backend - API -BACKEND_API_GRAPHQL_PLAYGROUND=false BACKEND_API_JWT_SECRET= BACKEND_API_JWT_EXPIRATION=360 # Backend - Worker diff --git a/docker/backend/.dockerignore b/backend/.dockerignore similarity index 100% rename from docker/backend/.dockerignore rename to backend/.dockerignore diff --git a/docker/backend/.editorconfig b/backend/.editorconfig similarity index 100% rename from docker/backend/.editorconfig rename to backend/.editorconfig diff --git a/docker/backend/.gitignore b/backend/.gitignore similarity index 100% rename from docker/backend/.gitignore rename to backend/.gitignore diff --git a/docker/backend/Dockerfile b/backend/Dockerfile similarity index 50% rename from docker/backend/Dockerfile rename to backend/Dockerfile index 7f8fbdc..a40a006 100644 --- a/docker/backend/Dockerfile +++ b/backend/Dockerfile @@ -14,50 +14,66 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -FROM crystallang/crystal:1.5-alpine as micrate-deps -WORKDIR /src +FROM veelenga/ameba:latest +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 ./ RUN shards install --production -FROM crystallang/crystal:1.5-alpine as micrate-builder -WORKDIR /src -# RUN apk add --no-cache sqlite-static -COPY --from=micrate-deps /src/shard.yml /src/shard.lock ./ -COPY --from=micrate-deps /src/lib ./lib +FROM crystallang/crystal:1.6-alpine 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 COPY ./micrate/src ./src RUN shards build --release --static --verbose -s -p -t FROM tdewolff/minify as public -WORKDIR /src +WORKDIR /usr/src/public COPY ./public ./src RUN minify -r -o ./dist ./src -FROM crystallang/crystal:1.5-alpine as deps -WORKDIR /src +FROM veelenga/ameba:latest +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 ./ RUN shards install --production -FROM crystallang/crystal:1.5-alpine as builder -ARG BUILD_ENV -WORKDIR /src/mentorenwahl +FROM crystallang/crystal:1.6-alpine as builder +WORKDIR /usr/src/mentorenwahl RUN apk add --no-cache pcre2-dev -COPY --from=deps /src/shard.yml /src/shard.lock ./ -COPY --from=deps /src/lib ./lib +COPY --from=deps /usr/src/mentorenwahl/shard.yml /usr/src/mentorenwahl/shard.lock ./ +COPY --from=deps /usr/src/mentorenwahl/lib ./lib COPY ./LICENSE . COPY ./Makefile . -COPY ./src ./src 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 \ make dev; \ else \ make; \ 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 -COPY --from=micrate-builder /src/bin/micrate /bin/micrate -COPY --from=builder /src/mentorenwahl/bin /bin -COPY --from=builder /src/mentorenwahl/db ./db +WORKDIR / +COPY --from=micrate-builder /usr/src/micrate/bin/micrate ./bin/micrate +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 -ENTRYPOINT [ "backend" ] +ENTRYPOINT [ "./bin/backend" ] CMD [ "run" ] diff --git a/docker/backend/LICENSE b/backend/LICENSE similarity index 100% rename from docker/backend/LICENSE rename to backend/LICENSE diff --git a/docker/backend/Makefile b/backend/Makefile similarity index 83% rename from docker/backend/Makefile rename to backend/Makefile index e5c573d..9c33eb6 100644 --- a/docker/backend/Makefile +++ b/backend/Makefile @@ -19,10 +19,10 @@ all: prod dev: - shards build -Ddevelopment --static --verbose -s -p -t + shards build -Dplayground --verbose -s -p -t prod: - shards build --static --release --verbose -s -p -t + shards build --production --static --release --verbose -s -p -t docs: - crystal docs --project-name "Mentorenwahl Backend" + crystal docs --project-name "Mentorenwahl" diff --git a/docker/backend/db/migrations/20220414171336_create_users.sql b/backend/db/migrations/20220414171336_create_users.sql similarity index 93% rename from docker/backend/db/migrations/20220414171336_create_users.sql rename to backend/db/migrations/20220414171336_create_users.sql index f29bfb1..e489e42 100644 --- a/docker/backend/db/migrations/20220414171336_create_users.sql +++ b/backend/db/migrations/20220414171336_create_users.sql @@ -17,14 +17,14 @@ */ -- +micrate Up -- 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( id SERIAL PRIMARY KEY, username TEXT UNIQUE 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( diff --git a/docker/backend/micrate/.editorconfig b/backend/micrate/.editorconfig similarity index 100% rename from docker/backend/micrate/.editorconfig rename to backend/micrate/.editorconfig diff --git a/docker/backend/micrate/.gitignore b/backend/micrate/.gitignore similarity index 100% rename from docker/backend/micrate/.gitignore rename to backend/micrate/.gitignore diff --git a/docker/backend/micrate/shard.lock b/backend/micrate/shard.lock similarity index 100% rename from docker/backend/micrate/shard.lock rename to backend/micrate/shard.lock diff --git a/docker/backend/micrate/shard.yml b/backend/micrate/shard.yml similarity index 100% rename from docker/backend/micrate/shard.yml rename to backend/micrate/shard.yml diff --git a/docker/backend/micrate/src/micrate.cr b/backend/micrate/src/micrate.cr similarity index 100% rename from docker/backend/micrate/src/micrate.cr rename to backend/micrate/src/micrate.cr diff --git a/docker/backend/public/index.html b/backend/public/index.html similarity index 100% rename from docker/backend/public/index.html rename to backend/public/index.html diff --git a/docker/backend/shard.lock b/backend/shard.lock similarity index 95% rename from docker/backend/shard.lock rename to backend/shard.lock index 27fc376..f2e10f9 100644 --- a/docker/backend/shard.lock +++ b/backend/shard.lock @@ -6,7 +6,7 @@ shards: athena: git: https://github.com/athena-framework/framework.git - version: 0.17.0 + version: 0.17.1 athena-config: git: https://github.com/athena-framework/config.git @@ -30,15 +30,15 @@ shards: athena-routing: git: https://github.com/athena-framework/routing.git - version: 0.1.2 + version: 0.1.3 athena-serializer: git: https://github.com/athena-framework/serializer.git - version: 0.3.0 + version: 0.3.1 athena-validator: git: https://github.com/athena-framework/validator.git - version: 0.2.0 + version: 0.2.1 baked_file_system: git: https://github.com/schovi/baked_file_system.git @@ -46,7 +46,7 @@ shards: bindata: git: https://github.com/spider-gazelle/bindata.git - version: 1.10.0 + version: 1.11.0 clear: git: https://github.com/vici37/clear.git @@ -82,7 +82,7 @@ shards: graphql: git: https://github.com/graphql-crystal/graphql.git - version: 0.4.0 + version: 0.4.0+git.commit.e3281bb0ef0ca301ccea176e6839422ac766465b habitat: git: https://github.com/luckyframework/habitat.git @@ -126,7 +126,7 @@ shards: pretty: git: https://github.com/maiha/pretty.cr.git - version: 1.1.1 + version: 1.1.2 promise: git: https://github.com/spider-gazelle/promise.git @@ -150,7 +150,7 @@ shards: shard: git: https://github.com/maiha/shard.cr.git - version: 0.3.1 + version: 1.0.0 version_from_shard: git: https://github.com/hugopl/version_from_shard.git diff --git a/docker/backend/shard.yml b/backend/shard.yml similarity index 97% rename from docker/backend/shard.yml rename to backend/shard.yml index a3a9924..3c5da78 100644 --- a/docker/backend/shard.yml +++ b/backend/shard.yml @@ -24,9 +24,9 @@ license: GNU GPLv3 targets: backend: - main: src/cli/backend.cr + main: src/backend.cr -crystal: 1.5.0 +crystal: 1.6.1 dependencies: clear: @@ -34,6 +34,7 @@ dependencies: branch: master graphql: github: graphql-crystal/graphql + branch: main jwt: github: crystal-community/jwt commander: diff --git a/docker/backend/src/backend.cr b/backend/src/backend.cr similarity index 90% rename from docker/backend/src/backend.cr rename to backend/src/backend.cr index 1a39e68..882c582 100644 --- a/docker/backend/src/backend.cr +++ b/backend/src/backend.cr @@ -14,8 +14,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +require "commander" +require "docker" + require "./backend/*" # Base module module Backend + Docker.setup + Db.init + + Commander.run(CLI, ARGV) end diff --git a/docker/backend/src/backend/api.cr b/backend/src/backend/api.cr similarity index 100% rename from docker/backend/src/backend/api.cr rename to backend/src/backend/api.cr diff --git a/backend/src/backend/api/auth.cr b/backend/src/backend/api/auth.cr new file mode 100644 index 0000000..fa482a6 --- /dev/null +++ b/backend/src/backend/api/auth.cr @@ -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 . + +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 diff --git a/docker/backend/src/backend/api/context.cr b/backend/src/backend/api/context.cr similarity index 54% rename from docker/backend/src/backend/api/context.cr rename to backend/src/backend/api/context.cr index 5219d5c..3359f01 100644 --- a/docker/backend/src/backend/api/context.cr +++ b/backend/src/backend/api/context.cr @@ -16,6 +16,8 @@ require "graphql" require "http/headers" +require "jwt/errors" +require "json" module Backend module Api @@ -24,6 +26,9 @@ module Backend # Development mode getter development + # Request status + getter status + # Authenticated user getter user @@ -38,6 +43,7 @@ module Backend def initialize( @development : Bool, + @status : Status, @user : Db::User?, @admin : Bool?, @role : Schema::UserRole?, @@ -45,37 +51,54 @@ module Backend ) end - def initialize(headers : HTTP::Headers, @development : Bool, *rest) + def initialize(headers : HTTP::Headers, @development : Bool, @status = Status::OK, *rest) super(*rest) - if (token = headers["authorization"]?) && token[..Auth::BEARER.size - 1] == Auth::BEARER - payload = Auth.decode_jwt?(token[Auth::BEARER.size..]) - return unless payload - - data = payload["data"].as_h - return unless @user = Db::User.find(data["user_id"].as_i) - - if @user - @admin = user.not_nil!.admin - @role = user.not_nil!.role.to_api - @external = - case @role.not_nil! - when .teacher? - @user.not_nil!.teacher - when .student? - @user.not_nil!.student - end + if (token = headers["authorization"]?) && token.starts_with?(Auth::BEARER) + begin + payload = Auth::Token.decode(token[Auth::BEARER.size..].strip) + rescue ex : JWT::ExpiredSignatureError + @status = Status::SessionExpired + rescue + @status = Status::JWTError + else + if @user = Db::User.find(payload.context.user) + @admin = user.not_nil!.admin + @role = user.not_nil!.role.to_api + @external = + case @role.not_nil! + when .teacher? + @user.not_nil!.teacher + when .student? + @user.not_nil!.student + 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 def authenticated? : Bool - !!@user + !!@user && @status.ok? end # :ditto: def authenticated! : Bool + raise "Session expired" if @status.session_expired? raise "Not authenticated" unless authenticated? true @@ -88,20 +111,21 @@ module Backend # :ditto: def admin! : Bool + authenticated! raise "Invalid permissions" unless admin? true end # 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? roles.each do |role| return true if @role == role && if external_check 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 Schema::UserRole::Teacher when Db::Student @@ -116,33 +140,64 @@ module Backend end # :ditto: - def role!(external_check = true, *roles : Schema::UserRole) : Bool - raise "Invalid permissions" unless role? external, *roles + def role!(roles : Array(Schema::UserRole), external_check = true) : Bool + authenticated! + raise "Invalid permissions" unless role?(roles, external_check) true end # User is teacher def teacher?(external_check = true) : Bool - role? external_check, Schema::UserRole::Teacher + role?([Schema::UserRole::Teacher], external_check) end # :ditto: def teacher!(external_check = true) : Bool - role! external_check, Schema::UserRole::Teacher + role!([Schema::UserRole::Teacher], external_check) end # User is student def student?(external_check = true) : Bool - role? external_check, Schema::UserRole::Student + role?([Schema::UserRole::Student], external_check) end # :ditto: def student!(external_check = true) : Bool - role! external_check, Schema::UserRole::Student + role!([Schema::UserRole::Student], external_check) 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 diff --git a/backend/src/backend/api/errors.cr b/backend/src/backend/api/errors.cr new file mode 100644 index 0000000..54b6008 --- /dev/null +++ b/backend/src/backend/api/errors.cr @@ -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 diff --git a/docker/backend/src/backend/api/schema.cr b/backend/src/backend/api/schema.cr similarity index 100% rename from docker/backend/src/backend/api/schema.cr rename to backend/src/backend/api/schema.cr diff --git a/docker/backend/src/backend/api/schema/config.cr b/backend/src/backend/api/schema/config.cr similarity index 100% rename from docker/backend/src/backend/api/schema/config.cr rename to backend/src/backend/api/schema/config.cr diff --git a/docker/backend/src/backend/api/schema/helpers.cr b/backend/src/backend/api/schema/helpers.cr similarity index 100% rename from docker/backend/src/backend/api/schema/helpers.cr rename to backend/src/backend/api/schema/helpers.cr diff --git a/docker/backend/src/backend/api/schema/mutation.cr b/backend/src/backend/api/schema/mutation.cr similarity index 56% rename from docker/backend/src/backend/api/schema/mutation.cr rename to backend/src/backend/api/schema/mutation.cr index 5078f91..b676478 100644 --- a/docker/backend/src/backend/api/schema/mutation.cr +++ b/backend/src/backend/api/schema/mutation.cr @@ -15,6 +15,7 @@ # along with this program. If not, see . require "ldap" +require "uuid" module Backend module Api @@ -24,17 +25,21 @@ module Backend @[GraphQL::Field] # Logs in as *username* with credential *password* 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 } - 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( user: User.new(user), - token: Auth.create_user_jwt( - user.id.not_nil!.to_i, - (Time.utc + Backend.config.api.jwt_expiration.minutes).to_unix - ), + token: Auth::Token.new( + iss: "mentorenwahl", + 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 @@ -48,7 +53,7 @@ module Backend rescue LDAP::Client::AuthError true 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 User.new(user) @@ -65,16 +70,6 @@ module Backend id 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] # Starts assignment job of mentors to students def assign_students(context : Context) : Bool @@ -85,68 +80,68 @@ module Backend true end - @[GraphQL::Field] - # Creates teacher - def create_teacher(context : Context, input : TeacherCreateInput) : Teacher - context.admin! + # @[GraphQL::Field] + # # Creates teacher + # def create_teacher(context : Context, input : TeacherCreateInput) : Teacher + # context.admin! - teacher = Db::Teacher.create!(user_id: input.user_id, max_students: input.max_students) - Teacher.new(teacher) - end + # teacher = Db::Teacher.create!(user_id: input.user_id, max_students: input.max_students) + # Teacher.new(teacher) + # end - @[GraphQL::Field] - # Deletes teacher by ID - def delete_teacher(context : Context, id : Int32) : Int32 - context.admin! + # @[GraphQL::Field] + # # Deletes teacher by ID + # def delete_teacher(context : Context, id : Int32) : Int32 + # context.admin! - teacher = Db::Teacher.find!(id) - teacher.delete + # teacher = Db::Teacher.find!(id) + # teacher.delete - id - end + # 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 - def create_student(context : Context, input : StudentCreateInput) : Student - context.admin! + # @[GraphQL::Field] + # # Creates student + # def create_student(context : Context, input : StudentCreateInput) : Student + # context.admin! - user = Db::User.find!(input.user_id) - raise "User not a student" unless user.role.to_api.student? + # user = Db::User.find!(input.user_id) + # raise "User not a student" unless user.role.to_api.student? - student = Db::Student.create!(user_id: user.id) - Student.new(student) - end + # student = Db::Student.create!(user_id: user.id) + # Student.new(student) + # end - @[GraphQL::Field] - # Deletes student by ID - def delete_student(context : Context, id : Int32) : Int32 - context.admin! + # @[GraphQL::Field] + # # Deletes student by ID + # def delete_student(context : Context, id : Int32) : Int32 + # context.admin! - student = Db::Student.find!(id) - student.delete + # student = Db::Student.find!(id) + # student.delete - id - end + # id + # end - @[GraphQL::Field] - # Self register as student - def register_student(context : Context) : Student - context.student! external_check: false + # @[GraphQL::Field] + # # Self register as student + # def register_student(context : Context) : Student + # context.student! external_check: false - Student.new( - Db::Student.create!(user_id: context.user.not_nil!.id) - ) - end + # Student.new( + # Db::Student.create!(user_id: context.user.not_nil!.id) + # ) + # end @[GraphQL::Field] # Creates vote for authenticated user's student @@ -163,12 +158,12 @@ module Backend if teacher.nil? raise "Teachers not found" - elsif teacher.user.skif != context.user.not_nil!.skif - if teacher.user.skif - raise "Teacher is SKIF, student is not" - else - raise "Teacher is not SKIF, student is" - end + # elsif teacher.user.skif != context.user.not_nil!.skif + # if teacher.user.skif + # raise "Teacher is SKIF, student is not" + # else + # raise "Teacher is not SKIF, student is" + # end end end diff --git a/docker/backend/src/backend/api/schema/query.cr b/backend/src/backend/api/schema/query.cr similarity index 100% rename from docker/backend/src/backend/api/schema/query.cr rename to backend/src/backend/api/schema/query.cr diff --git a/docker/backend/src/backend/api/schema/student.cr b/backend/src/backend/api/schema/student.cr similarity index 100% rename from docker/backend/src/backend/api/schema/student.cr rename to backend/src/backend/api/schema/student.cr diff --git a/docker/backend/src/backend/api/schema/teacher.cr b/backend/src/backend/api/schema/teacher.cr similarity index 100% rename from docker/backend/src/backend/api/schema/teacher.cr rename to backend/src/backend/api/schema/teacher.cr diff --git a/docker/backend/src/backend/api/schema/teacher_vote.cr b/backend/src/backend/api/schema/teacher_vote.cr similarity index 100% rename from docker/backend/src/backend/api/schema/teacher_vote.cr rename to backend/src/backend/api/schema/teacher_vote.cr diff --git a/docker/backend/src/backend/api/schema/user.cr b/backend/src/backend/api/schema/user.cr similarity index 85% rename from docker/backend/src/backend/api/schema/user.cr rename to backend/src/backend/api/schema/user.cr index 94c9119..2a208df 100644 --- a/docker/backend/src/backend/api/schema/user.cr +++ b/backend/src/backend/api/schema/user.cr @@ -25,7 +25,12 @@ module Backend # DB representation of the enum 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 # GraphQL representation of the DB enum @@ -45,7 +50,7 @@ module Backend # LDAP user data 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 raw_cache = Redis::CLIENT.get("ldap:user:#{id}").not_nil! end @@ -67,8 +72,8 @@ module Backend @[GraphQL::Field] # User's full name - def name : String - ldap.name + def name(formal : Bool = true) : String + ldap.name(formal) end @[GraphQL::Field] @@ -95,40 +100,27 @@ module Backend @model.role.to_api end - @[GraphQL::Field] - # User is at SKIF - def skif : Bool - @model.skif - end - @[GraphQL::Field] # User's external ID - def external_id : Int32? + def external_id : Int32 case @model.role.to_api - when .teacher? + when Db::UserRole::Teacher @model.teacher - when .student? + when Db::UserRole::Student @model.student - end - .try(&.id.try(&.to_i)) + end.not_nil!.id end @[GraphQL::Field] # User's external teacher object def teacher : Teacher? - teacher = @model.teacher - if teacher - Teacher.new(teacher) - end + @model.teacher.try { |t| Teacher.new(t) } end @[GraphQL::Field] # User's external student object def student : Student? - student = @model.student - if student - Student.new(student) - end + @model.student.try { |s| Student.new(s) } end end @@ -137,14 +129,12 @@ module Backend class UserCreateInput < GraphQL::BaseInputObject getter username getter role - getter skif getter admin @[GraphQL::Field] def initialize( @username : String, @role : UserRole, - @skif : Bool, @admin : Bool = false ) end diff --git a/docker/backend/src/backend/api/schema/vote.cr b/backend/src/backend/api/schema/vote.cr similarity index 100% rename from docker/backend/src/backend/api/schema/vote.cr rename to backend/src/backend/api/schema/vote.cr diff --git a/docker/backend/src/backend/authors.cr b/backend/src/backend/authors.cr similarity index 100% rename from docker/backend/src/backend/authors.cr rename to backend/src/backend/authors.cr diff --git a/docker/backend/src/backend/cli.cr b/backend/src/backend/cli.cr similarity index 87% rename from docker/backend/src/backend/cli.cr rename to backend/src/backend/cli.cr index 7296448..4454f18 100644 --- a/docker/backend/src/backend/cli.cr +++ b/backend/src/backend/cli.cr @@ -1,5 +1,4 @@ require "commander" -require "compiled_license" module Backend CLI = Commander::Command.new do |cmd| @@ -36,7 +35,7 @@ module Backend c.long = c.short c.run do - puts CompiledLicense::LICENSES + puts LICENSES end end @@ -55,13 +54,6 @@ module Backend 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" @@ -85,7 +77,14 @@ module Backend abort unless gets(chomp: true).not_nil!.strip.downcase == "y" 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 puts "Done!" diff --git a/docker/backend/src/backend/config.cr b/backend/src/backend/config.cr similarity index 88% rename from docker/backend/src/backend/config.cr rename to backend/src/backend/config.cr index d3e9407..96a9b2b 100644 --- a/docker/backend/src/backend/config.cr +++ b/backend/src/backend/config.cr @@ -32,25 +32,25 @@ module Backend # Types of environments program can compiled for / with enum BuildEnv - Production + Release Development end # Type of environment program is running in def build_env : BuildEnv - {{ flag?(:development) }} ? BuildEnv::Development : BuildEnv::Production + {{ flag?(:release) }} ? BuildEnv::Release : BuildEnv::Development end - # Production mode + # Release mode # - # `true` if the build environment is `BuildEnv::Development` - def production? : Bool + # `true` if the build environment is `BuildEnv::Release` + def release? : Bool build_env.production? end # Development mode # - # `true` if the build environment is `BuildEnv::Production` + # `true` if the build environment is `BuildEnv::Development` def development? : Bool build_env.development? end @@ -87,20 +87,15 @@ module Backend class ApiConfig include EnvConfig - # GraphQL playground enable - getter graphql_playground : Bool - # JWT signing key getter jwt_secret : String # JWT expiration time in minutes getter jwt_expiration : Int32 - # Helper method for enabling GraphQL playground - # - # Returns `true` if `Config#development?` or `#graphql_playground` are - def graphql_playground_fully_enabled? : Bool - Backend.config.development? || graphql_playground + # Returns true of `playground` flag was set on compile time + def graphql_playground? : Bool + flag?(:playground) end end diff --git a/docker/backend/src/backend/db.cr b/backend/src/backend/db.cr similarity index 91% rename from docker/backend/src/backend/db.cr rename to backend/src/backend/db.cr index c73f936..3e5eb5a 100644 --- a/docker/backend/src/backend/db.cr +++ b/backend/src/backend/db.cr @@ -29,7 +29,7 @@ module Backend # Migration UIDs 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 Retriable.retry(on: DB::ConnectionRefused, backoff: false) do Clear::SQL.init(Backend.config.db.url) diff --git a/docker/backend/src/backend/db/assignment.cr b/backend/src/backend/db/assignment.cr similarity index 100% rename from docker/backend/src/backend/db/assignment.cr rename to backend/src/backend/db/assignment.cr diff --git a/docker/backend/src/backend/db/micrate_db_version.cr b/backend/src/backend/db/micrate_db_version.cr similarity index 100% rename from docker/backend/src/backend/db/micrate_db_version.cr rename to backend/src/backend/db/micrate_db_version.cr diff --git a/docker/backend/src/backend/db/student.cr b/backend/src/backend/db/student.cr similarity index 100% rename from docker/backend/src/backend/db/student.cr rename to backend/src/backend/db/student.cr diff --git a/docker/backend/src/backend/db/teacher.cr b/backend/src/backend/db/teacher.cr similarity index 100% rename from docker/backend/src/backend/db/teacher.cr rename to backend/src/backend/db/teacher.cr diff --git a/docker/backend/src/backend/db/teacher_vote.cr b/backend/src/backend/db/teacher_vote.cr similarity index 100% rename from docker/backend/src/backend/db/teacher_vote.cr rename to backend/src/backend/db/teacher_vote.cr diff --git a/docker/backend/src/backend/db/user.cr b/backend/src/backend/db/user.cr similarity index 72% rename from docker/backend/src/backend/db/user.cr rename to backend/src/backend/db/user.cr index 8b9e45e..e959c9f 100644 --- a/docker/backend/src/backend/db/user.cr +++ b/backend/src/backend/db/user.cr @@ -5,7 +5,14 @@ module Backend struct UserRole # API representation of the enum 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 # DB representation of the enum @@ -23,7 +30,7 @@ module Backend column username : String column role : UserRole column admin : Bool = false - column skif : Bool + column jti : UUID? has_one student : Student?, foreign_key: :user_id has_one teacher : Teacher?, foreign_key: :user_id diff --git a/docker/backend/src/backend/db/vote.cr b/backend/src/backend/db/vote.cr similarity index 100% rename from docker/backend/src/backend/db/vote.cr rename to backend/src/backend/db/vote.cr diff --git a/docker/backend/src/backend/ldap.cr b/backend/src/backend/ldap.cr similarity index 100% rename from docker/backend/src/backend/ldap.cr rename to backend/src/backend/ldap.cr diff --git a/docker/backend/src/backend/ldap/dn.cr b/backend/src/backend/ldap/dn.cr similarity index 100% rename from docker/backend/src/backend/ldap/dn.cr rename to backend/src/backend/ldap/dn.cr diff --git a/docker/backend/src/backend/ldap/user.cr b/backend/src/backend/ldap/user.cr similarity index 89% rename from docker/backend/src/backend/ldap/user.cr rename to backend/src/backend/ldap/user.cr index 8bd7cd5..94d1221 100644 --- a/docker/backend/src/backend/ldap/user.cr +++ b/backend/src/backend/ldap/user.cr @@ -26,22 +26,26 @@ module Backend @[JSON::Field(key: "givenName")] # First name - property first_name : String + getter first_name : String @[JSON::Field(key: "sn")] # Last name - property last_name : String + getter last_name : String @[JSON::Field(key: "mail")] # Email address - property email : String + getter email : String def initialize(@first_name : String, @last_name : String, @email : String) end # Name - def name : String - "#{first_name} #{last_name}" + def name(formal = true) : String + if formal + "#{@last_name}, #{@first_name}" + else + "#{@first_name} #{@last_name}" + end end # Creates user data from LDAP entry diff --git a/docker/backend/src/backend/license.cr b/backend/src/backend/license.cr similarity index 100% rename from docker/backend/src/backend/license.cr rename to backend/src/backend/license.cr diff --git a/backend/src/backend/licenses.cr b/backend/src/backend/licenses.cr new file mode 100644 index 0000000..e0a1ea8 --- /dev/null +++ b/backend/src/backend/licenses.cr @@ -0,0 +1,5 @@ +require "compiled_license" + +module Backend + LICENSES = CompiledLicense::LICENSES +end diff --git a/docker/backend/src/backend/log.cr b/backend/src/backend/log.cr similarity index 100% rename from docker/backend/src/backend/log.cr rename to backend/src/backend/log.cr diff --git a/docker/backend/src/backend/macros/migrations.cr b/backend/src/backend/macros/migrations.cr similarity index 100% rename from docker/backend/src/backend/macros/migrations.cr rename to backend/src/backend/macros/migrations.cr diff --git a/docker/backend/src/backend/mailers.cr b/backend/src/backend/mailers.cr similarity index 100% rename from docker/backend/src/backend/mailers.cr rename to backend/src/backend/mailers.cr diff --git a/docker/backend/src/backend/redis.cr b/backend/src/backend/redis.cr similarity index 100% rename from docker/backend/src/backend/redis.cr rename to backend/src/backend/redis.cr diff --git a/docker/backend/src/backend/runner.cr b/backend/src/backend/runner.cr similarity index 98% rename from docker/backend/src/backend/runner.cr rename to backend/src/backend/runner.cr index 7da4af5..40ef586 100644 --- a/docker/backend/src/backend/runner.cr +++ b/backend/src/backend/runner.cr @@ -36,7 +36,7 @@ module Backend # Run the backend def run : self - {% if flag?(:development) %} + {% if !flag?(:release) %} Log.warn { "Backend is running in development mode! Do not use this in production!" } {% end %} diff --git a/docker/backend/src/backend/version.cr b/backend/src/backend/version.cr similarity index 100% rename from docker/backend/src/backend/version.cr rename to backend/src/backend/version.cr diff --git a/docker/backend/src/backend/web.cr b/backend/src/backend/web.cr similarity index 100% rename from docker/backend/src/backend/web.cr rename to backend/src/backend/web.cr diff --git a/docker/backend/src/backend/web/controllers.cr b/backend/src/backend/web/controllers.cr similarity index 100% rename from docker/backend/src/backend/web/controllers.cr rename to backend/src/backend/web/controllers.cr diff --git a/docker/backend/src/backend/web/controllers/api_controller.cr b/backend/src/backend/web/controllers/api_controller.cr similarity index 61% rename from docker/backend/src/backend/web/controllers/api_controller.cr rename to backend/src/backend/web/controllers/api_controller.cr index 8de2203..74b2a73 100644 --- a/docker/backend/src/backend/web/controllers/api_controller.cr +++ b/backend/src/backend/web/controllers/api_controller.cr @@ -15,7 +15,7 @@ # along with this program. If not, see . require "http/headers" -require "mime" +require "baked_file_system" module Backend module Web @@ -23,25 +23,29 @@ module Backend @[ARTA::Route(path: "/")] # GraphQL API controller class ApiController < ATH::Controller - @[ARTA::Get("")] - def playground : ATH::Response | ATH::Exceptions::HTTPException - {% if flag?(:development) %} - ATH::StreamedResponse.new(headers: HTTP::Headers{"content-type" => MIME.from_extension(".html")}) do |io| + {% if flag?(:playground) %} + # Public folder virtual filesystem + private module Public + 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) end - {% else %} - if Backend.config.api.graphql_playground_fully_enabled? - ATH::StreamedResponse.new(headers: HTTP::Headers{"content-type" => MIME.from_extension(".html")}) do |io| - IO.copy(Public.get("index.html"), io) - end - else - ATH::Exceptions::ServiceUnavailable.new("GraphQL Playground is not enabled. Please enable it in the backend configuration.") - end - {% end %} - end + end + {% else %} + @[ARTA::Get("")] + def playground : ATH::Exceptions::ServiceUnavailable + ATH::Exceptions::ServiceUnavailable.new("GraphQL playground is disabled") + end + {% end %} - # GraphQL query request data - struct GraphQLQueryData + # GraphQL query data + struct GraphQLQuery include JSON::Serializable # Raw query @@ -55,15 +59,18 @@ module Backend end @[ARTA::Post("")] - @[ATHA::QueryParam("development")] - def endpoint(request : ATH::Request, development : Bool = false) : ATH::Response - {% if flag?(:development) %} + {% 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 %} - 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( io, query.query, diff --git a/docker/backend/src/backend/web/service.cr b/backend/src/backend/web/service.cr similarity index 100% rename from docker/backend/src/backend/web/service.cr rename to backend/src/backend/web/service.cr diff --git a/docker/backend/src/backend/worker.cr b/backend/src/backend/worker.cr similarity index 61% rename from docker/backend/src/backend/worker.cr rename to backend/src/backend/worker.cr index 571c516..c3a6739 100644 --- a/docker/backend/src/backend/worker.cr +++ b/backend/src/backend/worker.cr @@ -21,23 +21,23 @@ require "./worker/*" module Backend # Worker module module Worker - # :inherit: - module Mosquito::Serializers::Granite - # :inherit: - macro serialize_granite_model(klass) - {% method_suffix = klass.resolve.stringify.underscore.gsub(/::/, "__").id %} + # # :inherit: + # module Mosquito::Serializers::Granite + # # :inherit: + # macro serialize_granite_model(klass) + # {% method_suffix = klass.resolve.stringify.underscore.gsub(/::/, "__").id %} - # Serializes {{ klaas.id }} to redis manageable data - def serialize_{{ method_suffix }}(model : {{ klass.id }}) : String - model.id.to_s - end + # # Serializes {{ klaas.id }} to redis manageable data + # def serialize_{{ method_suffix }}(model : {{ klass.id }}) : String + # model.id.to_s + # end - # Deserializes {{ klaas.id }} from redis manageable data - def deserialize_{{ method_suffix }}(raw : String) : {{ klass.id }} - {{ klass.id }}.find!(raw.to_i) - end - end - end + # # Deserializes {{ klaas.id }} from redis manageable data + # def deserialize_{{ method_suffix }}(raw : String) : {{ klass.id }} + # {{ klass.id }}.find!(raw.to_i) + # end + # end + # end Mosquito.configure do |settings| settings.redis_url = Backend.config.redis.url diff --git a/docker/backend/src/backend/worker/jobs.cr b/backend/src/backend/worker/jobs.cr similarity index 100% rename from docker/backend/src/backend/worker/jobs.cr rename to backend/src/backend/worker/jobs.cr diff --git a/docker/backend/src/backend/worker/jobs/assignment_job.cr b/backend/src/backend/worker/jobs/assignment_job.cr similarity index 91% rename from docker/backend/src/backend/worker/jobs/assignment_job.cr rename to backend/src/backend/worker/jobs/assignment_job.cr index 2af2e69..42567ae 100644 --- a/docker/backend/src/backend/worker/jobs/assignment_job.cr +++ b/backend/src/backend/worker/jobs/assignment_job.cr @@ -72,10 +72,10 @@ module Backend # pp! possibilities - teacher_ids = Db::Teacher.query - .select("id") - .where { raw("(SELECT COUNT(*) FROM assignments WHERE teacher_id = teachers.id)") < max_students } - .map(&.id) + # teacher_ids = Db::Teacher.query + # .select("id") + # .where { raw("(SELECT COUNT(*) FROM assignments WHERE teacher_id = teachers.id)") < max_students } + # .map(&.id) students = Db::Student.query .where do raw("NOT EXISTS (SELECT 1 FROM assignments WHERE student_id = students.id)") & @@ -100,13 +100,12 @@ module Backend end 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 random_votes.shuffle!(Random::Secure) - votes = random_votes.dup - a = empty_assignment.clone - + # votes = random_votes.dup + # a = empty_assignment.clone end pp! assignments end diff --git a/docker/backend/src/backend/worker/jobs/cache_ldap_user_job.cr b/backend/src/backend/worker/jobs/cache_ldap_user_job.cr similarity index 100% rename from docker/backend/src/backend/worker/jobs/cache_ldap_user_job.cr rename to backend/src/backend/worker/jobs/cache_ldap_user_job.cr diff --git a/docker/backend/src/backend/worker/log.cr b/backend/src/backend/worker/log.cr similarity index 100% rename from docker/backend/src/backend/worker/log.cr rename to backend/src/backend/worker/log.cr diff --git a/docker/backend/src/backend/worker/service.cr b/backend/src/backend/worker/service.cr similarity index 100% rename from docker/backend/src/backend/worker/service.cr rename to backend/src/backend/worker/service.cr diff --git a/docker-compose.yml b/docker-compose.yml index 1952e10..bde2849 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,7 +19,6 @@ version: "3" services: nginx: image: nginx:alpine - container_name: nginx restart: always volumes: - ./config/nginx/nginx.conf:/etc/nginx/nginx.conf:ro @@ -32,7 +31,6 @@ services: db: image: postgres:alpine - container_name: db restart: always networks: - db @@ -44,7 +42,6 @@ services: adminer: image: adminer:standalone - container_name: adminer restart: always networks: - default @@ -54,7 +51,6 @@ services: redis: image: redis:alpine - container_name: redis restart: always networks: - redis @@ -62,11 +58,11 @@ services: - redis:/data backend: + image: mentorenwahl/backend build: - context: ./docker/backend + context: ./backend args: BUILD_ENV: production - container_name: backend restart: always networks: - default @@ -79,7 +75,6 @@ services: BACKEND_URL: ${URL} BACKEND_MINIMUM_TEACHER_SELECTION_COUNT: ${BACKEND_MINIMUM_TEACHER_SELECTION_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_EXPIRATION: ${BACKEND_API_JWT_EXPIRATION} BACKEND_SMTP_HELO: ${BACKEND_SMTP_HELO} @@ -101,9 +96,9 @@ services: BACKEND_LDAP_CACHE_REFRESH_INTERVAL: ${BACKEND_LDAP_CACHE_REFRESH_INTERVAL} frontend: + image: mentorenwahl/frontend build: - context: ./docker/frontend - container_name: frontend + context: ./frontend restart: always networks: - default @@ -116,6 +111,7 @@ networks: db: redis: + volumes: db: redis: diff --git a/docker/backend/src/backend/api/auth.cr b/docker/backend/src/backend/api/auth.cr deleted file mode 100644 index 36e0ba8..0000000 --- a/docker/backend/src/backend/api/auth.cr +++ /dev/null @@ -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 . - -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 diff --git a/docker/backend/src/backend/mailers/teacher_registration_mailer.cr b/docker/backend/src/backend/mailers/teacher_registration_mailer.cr deleted file mode 100644 index 7a9ee37..0000000 --- a/docker/backend/src/backend/mailers/teacher_registration_mailer.cr +++ /dev/null @@ -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 . - -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 diff --git a/docker/backend/src/backend/mailers/templates/teacher_registration_mailer.txt.ecr b/docker/backend/src/backend/mailers/templates/teacher_registration_mailer.txt.ecr deleted file mode 100644 index 38f0307..0000000 --- a/docker/backend/src/backend/mailers/templates/teacher_registration_mailer.txt.ecr +++ /dev/null @@ -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"] %> diff --git a/docker/backend/src/backend/public.cr b/docker/backend/src/backend/public.cr deleted file mode 100644 index e50ce1e..0000000 --- a/docker/backend/src/backend/public.cr +++ /dev/null @@ -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 . - -require "baked_file_system" - -module Backend - # Public folder virtual filesystem - module Public - extend BakedFileSystem - - bake_folder "../../public" - end -end diff --git a/docker/backend/src/backend/worker/jobs/send_teachers_registration_email_job.cr b/docker/backend/src/backend/worker/jobs/send_teachers_registration_email_job.cr deleted file mode 100644 index 52af323..0000000 --- a/docker/backend/src/backend/worker/jobs/send_teachers_registration_email_job.cr +++ /dev/null @@ -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 . - -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 diff --git a/docker/backend/src/cli/backend.cr b/docker/backend/src/cli/backend.cr deleted file mode 100644 index 3e92d4c..0000000 --- a/docker/backend/src/cli/backend.cr +++ /dev/null @@ -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 . - -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 " - 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) diff --git a/docker/frontend/.nvmrc b/docker/frontend/.nvmrc deleted file mode 100644 index d9f8800..0000000 --- a/docker/frontend/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -16.14.2 diff --git a/docker/frontend/.dockerignore b/frontend/.dockerignore similarity index 100% rename from docker/frontend/.dockerignore rename to frontend/.dockerignore diff --git a/docker/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs similarity index 100% rename from docker/frontend/.eslintrc.cjs rename to frontend/.eslintrc.cjs diff --git a/docker/frontend/.gitignore b/frontend/.gitignore similarity index 100% rename from docker/frontend/.gitignore rename to frontend/.gitignore diff --git a/docker/frontend/.npmrc b/frontend/.npmrc similarity index 100% rename from docker/frontend/.npmrc rename to frontend/.npmrc diff --git a/frontend/.nvmrc b/frontend/.nvmrc new file mode 100644 index 0000000..e0325e5 --- /dev/null +++ b/frontend/.nvmrc @@ -0,0 +1 @@ +v16.17.1 diff --git a/docker/frontend/.prettierrc b/frontend/.prettierrc similarity index 100% rename from docker/frontend/.prettierrc rename to frontend/.prettierrc diff --git a/docker/frontend/Dockerfile b/frontend/Dockerfile similarity index 86% rename from docker/frontend/Dockerfile rename to frontend/Dockerfile index 932ad37..b7d83a6 100644 --- a/docker/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,5 +1,5 @@ FROM node:16-alpine -WORKDIR /app +WORKDIR /usr/src/frontend COPY ./package.json ./yarn.lock ./ RUN yarn install --frozen-lockfile COPY . . diff --git a/docker/frontend/README.md b/frontend/README.md similarity index 100% rename from docker/frontend/README.md rename to frontend/README.md diff --git a/docker/frontend/package.json b/frontend/package.json similarity index 100% rename from docker/frontend/package.json rename to frontend/package.json diff --git a/docker/frontend/src/app.d.ts b/frontend/src/app.d.ts similarity index 100% rename from docker/frontend/src/app.d.ts rename to frontend/src/app.d.ts diff --git a/docker/frontend/src/app.html b/frontend/src/app.html similarity index 100% rename from docker/frontend/src/app.html rename to frontend/src/app.html diff --git a/docker/frontend/src/hooks.ts b/frontend/src/hooks.ts similarity index 100% rename from docker/frontend/src/hooks.ts rename to frontend/src/hooks.ts diff --git a/docker/frontend/src/lib/cookieNames.ts b/frontend/src/lib/cookieNames.ts similarity index 100% rename from docker/frontend/src/lib/cookieNames.ts rename to frontend/src/lib/cookieNames.ts diff --git a/docker/frontend/src/lib/graphql.ts b/frontend/src/lib/graphql.ts similarity index 80% rename from docker/frontend/src/lib/graphql.ts rename to frontend/src/lib/graphql.ts index 44c5108..cb6f96b 100644 --- a/docker/frontend/src/lib/graphql.ts +++ b/frontend/src/lib/graphql.ts @@ -4,10 +4,6 @@ export interface Node { id: ID; } -export interface Skif { - skif: boolean; -} - export enum UserRole { STUDENT = "Student", TEACHER = "Teacher", @@ -30,11 +26,11 @@ export interface UserExternal { user: User; } -export interface Student extends Node, UserExternal, Skif { +export interface Student extends Node, UserExternal { vote?: Vote; } -export interface Teacher extends Node, UserExternal, Skif { +export interface Teacher extends Node, UserExternal { maxStudents: number; } diff --git a/docker/frontend/src/routes/__layout.svelte b/frontend/src/routes/__layout.svelte similarity index 100% rename from docker/frontend/src/routes/__layout.svelte rename to frontend/src/routes/__layout.svelte diff --git a/docker/frontend/src/routes/index.svelte b/frontend/src/routes/index.svelte similarity index 59% rename from docker/frontend/src/routes/index.svelte rename to frontend/src/routes/index.svelte index 7bda3f4..5481e63 100644 --- a/docker/frontend/src/routes/index.svelte +++ b/frontend/src/routes/index.svelte @@ -47,81 +47,63 @@ `); query(meStore); - const teacherRegisterFormMaxStudents = svelteForms.field("maxStudents", 0, [ - validators.required(), - validators.min(0), - ]); - const teacherRegisterFormSkif = svelteForms.field("skif", false); - const teacheRegisterForm = svelteForms.form( - teacherRegisterFormMaxStudents, - teacherRegisterFormSkif - ); + // const teacherRegisterFormMaxStudents = svelteForms.field("maxStudents", 0, [ + // validators.required(), + // validators.min(0), + // ]); + // const teacheRegisterForm = svelteForms.form(teacherRegisterFormMaxStudents); - interface RegisterTeacherData { - registerTeacher: Teacher; - } + // interface RegisterTeacherData { + // registerTeacher: Teacher; + // } - interface RegisterTeacherVars { - maxStudents: number; - skif: boolean; - } + // interface RegisterTeacherVars { + // maxStudents: number; + // } - const registerTeacherStore = operationStore< - RegisterTeacherData, - RegisterTeacherVars - >(gql` - mutation RegisterTeacher($maxStudents: Int!, $skif: Boolean!) { - registerTeacher(input: { maxStudents: $maxStudents, skif: $skif }) { - id - } - } - `); + // const registerTeacherStore = operationStore< + // RegisterTeacherData, + // RegisterTeacherVars + // >(gql` + // mutation RegisterTeacher($maxStudents: Int!) { + // registerTeacher(input: { maxStudents: $maxStudents }) { + // id + // } + // } + // `); - const registerTeacherMutation = mutation(registerTeacherStore); + // const registerTeacherMutation = mutation(registerTeacherStore); - async function registerTeacher(): Promise { - await registerTeacherMutation({ - maxStudents: $teacherRegisterFormMaxStudents.value, - skif: $teacherRegisterFormSkif.value, - }); + // async function registerTeacher(): Promise { + // await registerTeacherMutation({ + // maxStudents: $teacherRegisterFormMaxStudents.value, + // }); - if (!$registerTeacherStore.error && $registerTeacherStore.data) { - location.reload(); - } - } + // if (!$registerTeacherStore.error && $registerTeacherStore.data) { + // location.reload(); + // } + // } - const registerStudentSkif = svelteForms.field("skif", false); - const registerStudentForm = svelteForms.form(registerStudentSkif); + // interface RegisterStudentData { + // registerStudent: Student; + // } - interface RegisterStudentData { - registerStudent: Student; - } + // const registerStudentStore = operationStore(gql` + // mutation RegisterStudent() { + // registerStudent() { + // id + // } + // } + // `); + // const registerStudentMutation = mutation(registerStudentStore); - interface RegisterStudentVars { - skif: boolean; - } + // async function registerStudent(): Promise { + // await registerStudentMutation(); - const registerStudentStore = operationStore< - RegisterStudentData, - RegisterStudentVars - >(gql` - mutation RegisterStudent($skif: Boolean!) { - registerStudent(input: { skif: $skif }) { - id - } - } - `); - const registerStudentMutation = mutation(registerStudentStore); - - async function registerStudent(): Promise { - await registerStudentMutation({ - skif: $registerStudentSkif.value, - }); - - if (!$registerStudentStore.error && $registerStudentStore.data) { - location.reload(); - } - } + // if (!$registerStudentStore.error && $registerStudentStore.data) { + // location.reload(); + // } + // } {#if $meStore.error} @@ -133,7 +115,7 @@
{#if $meStore.data.me.role === UserRole.TEACHER} - {#if !$meStore.data.me.teacher} + {:else if $meStore.data.me.role === UserRole.STUDENT} - {#if !$meStore.data.me.student} + {/if} {/if} diff --git a/docker/frontend/src/routes/login.svelte b/frontend/src/routes/login.svelte similarity index 100% rename from docker/frontend/src/routes/login.svelte rename to frontend/src/routes/login.svelte diff --git a/docker/frontend/static/.keep b/frontend/static/.keep similarity index 100% rename from docker/frontend/static/.keep rename to frontend/static/.keep diff --git a/docker/frontend/svelte.config.js b/frontend/svelte.config.js similarity index 100% rename from docker/frontend/svelte.config.js rename to frontend/svelte.config.js diff --git a/docker/frontend/tsconfig.json b/frontend/tsconfig.json similarity index 100% rename from docker/frontend/tsconfig.json rename to frontend/tsconfig.json diff --git a/docker/frontend/yarn.lock b/frontend/yarn.lock similarity index 100% rename from docker/frontend/yarn.lock rename to frontend/yarn.lock