diff --git a/.drone.yml b/.drone.yml index 9567f82..efde520 100644 --- a/.drone.yml +++ b/.drone.yml @@ -2,6 +2,7 @@ kind: pipeline type: docker name: default + steps: - name: shellcheck image: koalaman/shellcheck-alpine @@ -13,6 +14,7 @@ steps: kind: pipeline type: docker name: backend + steps: - name: ameba image: veelenga/ameba @@ -23,6 +25,25 @@ steps: image: boechat107/pgsanity commands: - pgsanity docker/backend/db/**/*.sql + - name: deps + image: crystallang/crystal:1.3-alpine + volumes: + - name: lib + path: /drone/src/docker/backend/lib + commands: + - cd docker/backend/ + - shards install + - name: documentation + image: crystallang/crystal:1.3-alpine + volumes: + - name: lib + path: /drone/src/docker/backend/lib + commands: + - cd docker/backend/ + - make docs + depends_on: + - ameba + - deps - name: build image: tmaier/docker-compose volumes: @@ -34,10 +55,15 @@ steps: depends_on: - ameba - pgsanity + - documentation + volumes: + - name: lib + temp: {} - name: dockersock host: path: /var/run/docker.sock + depends_on: - default @@ -45,6 +71,7 @@ depends_on: kind: pipeline type: docker name: frontend + steps: - name: prettier image: elnebuloso/prettier @@ -61,9 +88,11 @@ steps: - docker-compose build --build-arg BUILD_ENV=development frontend depends_on: - prettier + volumes: - name: dockersock host: path: /var/run/docker.sock + depends_on: - default diff --git a/.example.env b/.example.env index 63efdce..c38cd7f 100644 --- a/.example.env +++ b/.example.env @@ -23,6 +23,6 @@ BACKEND_SMTP_PASSWORD= BACKEND_LDAP_HOST= BACKEND_LDAP_PORT= BACKEND_LDAP_BASE_DN= +BACKEND_LDAP_BASE_USER_DN= BACKEND_BIND_DN= BACKEND_BIND_PASSWORD= -BACKEND_LDAP_USER_DN= diff --git a/README.md b/README.md index 4825a1c..ce73aae 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ # mentorenwahl A fullstack application for assigning mentors to students based on their whishes. + +# Documentation + +To build the documentation, run `make docs` in `docker/backend/`. diff --git a/docker-compose.yml b/docker-compose.yml index 7a5a5cf..71ba61b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -74,9 +74,9 @@ services: BACKEND_LDAP_HOST: ${BACKEND_LDAP_HOST} BACKEND_LDAP_PORT: ${BACKEND_LDAP_PORT} BACKEND_LDAP_BASE_DN: ${BACKEND_LDAP_BASE_DN} + BACKEND_LDAP_BASE_USER_DN: ${BACKEND_LDAP_BASE_USER_DN} BACKEND_LDAP_BIND_DN: ${BACKEND_LDAP_BIND_DN} BACKEND_LDAP_BIND_PASSWORD: ${BACKEND_LDAP_BIND_PASSWORD} - BACKEND_LDAP_USER_DN: ${BACKEND_LDAP_USER_DN} frontend: build: diff --git a/docker/backend/Dockerfile b/docker/backend/Dockerfile index 0063221..ffbf2b5 100644 --- a/docker/backend/Dockerfile +++ b/docker/backend/Dockerfile @@ -9,12 +9,13 @@ ARG BUILD_ENV WORKDIR /app/backend COPY --from=deps /app/shard.yml /app/shard.lock ./ COPY --from=deps /app/lib ./lib +COPY ./Makefile ./Makefile COPY ./LICENSE ./LICENSE COPY ./src ./src RUN if [ "${BUILD_ENV}" = "development" ]; then \ - time shards build -Ddevelopment --static --verbose -s -p -t; \ + make dev; \ else \ - time shards build --static --release --no-debug --verbose -s -p -t; \ + make; \ fi FROM alpine as runner diff --git a/docker/backend/Makefile b/docker/backend/Makefile new file mode 100644 index 0000000..c27b736 --- /dev/null +++ b/docker/backend/Makefile @@ -0,0 +1,12 @@ +.PHONY: all dev prod docs + +all: prod + +dev: + shards build -Ddevelopment --static --verbose -s -p -t + +prod: + shards build --static --release --no-debug --verbose -s -p -t + +docs: + crystal docs --project-name "Mentorenwahl Backend" -D granite_docs diff --git a/docker/backend/src/backend.cr b/docker/backend/src/backend.cr index 417e3ef..4295edf 100644 --- a/docker/backend/src/backend.cr +++ b/docker/backend/src/backend.cr @@ -1 +1,5 @@ require "./backend/*" + +# Base module +module Backend +end diff --git a/docker/backend/src/backend/api.cr b/docker/backend/src/backend/api.cr index f2c0573..34e5d53 100644 --- a/docker/backend/src/backend/api.cr +++ b/docker/backend/src/backend/api.cr @@ -1 +1,5 @@ require "./api/*" + +# Api module +module Backend::Api +end diff --git a/docker/backend/src/backend/api/auth.cr b/docker/backend/src/backend/api/auth.cr index 533e303..6d2fd0f 100644 --- a/docker/backend/src/backend/api/auth.cr +++ b/docker/backend/src/backend/api/auth.cr @@ -2,28 +2,36 @@ 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 - def create_user_jwt(user_id : Int, expiration : Int) : String - create_jwt({user_id: user_id}, expiration) - 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/api/context.cr b/docker/backend/src/backend/api/context.cr index 73d6675..b935eca 100644 --- a/docker/backend/src/backend/api/context.cr +++ b/docker/backend/src/backend/api/context.cr @@ -4,10 +4,18 @@ require "granite" module Backend module Api + # GraphQL request context class class Context < GraphQL::Context + # Authenticated user getter user : Db::User? + + # User is admin getter admin : Bool? + + # User's role getter role : Schema::UserRole? + + # User's external object getter external : (Db::Teacher | Db::Student)? def initialize(request : HTTP::Request, max_complexity : Int32? = nil) @@ -36,26 +44,31 @@ module Backend end end + # User is authenticated def authenticated? : Bool !!@user end + # :ditto: def authenticated! : Bool raise "Not authenticated" unless authenticated? true end + # User is admin def admin? : Bool authenticated? && !!@admin end + # :ditto: def admin! : Bool raise "Invalid permissions" unless admin? true end + # User's is one of *roles* def role?(external = true, *roles : Schema::UserRole) : Bool return false unless authenticated? @@ -75,51 +88,32 @@ module Backend false end + # :ditto: def role!(external = true, *roles : Schema::UserRole) : Bool raise "Invalid permissions" unless role? external, *roles true end + # User is teacher def teacher?(external = false) : Bool role? external, Schema::UserRole::Teacher end + # :ditto: def teacher! : Bool role! external, Schema::UserRole::Teacher end + # User is student def student?(external = false) : Bool role? external, Schema::UserRole::Student end + # :ditto: def student! : Bool role! external, Schema::UserRole::Student end - - # private macro role_check(*roles) - # {% for role in roles %} - # {% name = role.names.last.underscore %} - # def {{ name }}?(external = true) : Bool - # role? external, {{ role }} - # end - - # def {{ name }}!(external = true) : Bool - # role! external, {{ role }} - # end - # {% end %} - # end - - # role_check Schema::UserRole::Teacher, Schema::UserRole::Student - - # def self.db_eq_role?(external : Granite::Base, role : Schema::UserRole) : Bool - # role == case external - # when Db::Teacher - # Schema::UserRole::Teacher - # when Db::Student - # Schema::UserRole::Student - # end - # end end end end diff --git a/docker/backend/src/backend/api/run.cr b/docker/backend/src/backend/api/run.cr index 5dc71bc..4b1a733 100644 --- a/docker/backend/src/backend/api/run.cr +++ b/docker/backend/src/backend/api/run.cr @@ -4,6 +4,7 @@ module Backend module Api extend self + # Runs API def run : Nil WebServer.new.run end diff --git a/docker/backend/src/backend/api/schema.cr b/docker/backend/src/backend/api/schema.cr index 9468dda..106f396 100644 --- a/docker/backend/src/backend/api/schema.cr +++ b/docker/backend/src/backend/api/schema.cr @@ -5,7 +5,9 @@ require "./schema/*" module Backend module Api + # GraphQL schema definitions module Schema + # Compiled GraphQL schema SCHEMA = GraphQL::Schema.new(Query.new, Mutation.new) end end diff --git a/docker/backend/src/backend/api/schema/helpers.cr b/docker/backend/src/backend/api/schema/helpers.cr index aace3b7..736d050 100644 --- a/docker/backend/src/backend/api/schema/helpers.cr +++ b/docker/backend/src/backend/api/schema/helpers.cr @@ -1,8 +1,11 @@ module Backend module Api module Schema + # Schema helper macros module Helpers + # Object helpers module ObjectMacros + # Defines field property and GraphQL specific getter macro field(type) property {{ type.var }} {% if type.value %} = {{ type.value }}{% end %} @@ -13,7 +16,9 @@ module Backend end end + # DB model leverage helpers module ObjectDbInit + # Defines a DB model specific initializer macro db_init(type) def initialize(obj : {{ type }}) initialize(obj.id.not_nil!) @@ -21,12 +26,16 @@ module Backend end end + # DB model finder helpers module ObjectFinders + # Defines finder macro finders(type) + # Finds object by ID def find : {{ type }}? {{ type }}.find(@id) end + # :ditto: def find! : {{ type }} obj = find raise "#{{{ type }}} not found" unless obj @@ -36,8 +45,12 @@ module Backend end end + # DB model field helpers module DbObject + # Defines DB model field helper functions macro db_object(type) + {% space_name = type.names.last.underscore.gsub(/_/, " ").capitalize %} + include ::Backend::Api::Schema::Helpers::ObjectDbInit include ::Backend::Api::Schema::Helpers::ObjectFinders @@ -54,6 +67,7 @@ module Backend end @[GraphQL::Field] + # {{ space_name }}'s ID def id : Int32 @id end diff --git a/docker/backend/src/backend/api/schema/mutation.cr b/docker/backend/src/backend/api/schema/mutation.cr index 219d931..4557f75 100644 --- a/docker/backend/src/backend/api/schema/mutation.cr +++ b/docker/backend/src/backend/api/schema/mutation.cr @@ -6,6 +6,7 @@ module Backend @[GraphQL::Object] class Mutation < GraphQL::BaseMutation @[GraphQL::Field] + # Logs in as *username* with credential *password* def login(username : String, password : String) : LoginPayload raise "Auth failed" if username.empty? || password.empty? @@ -22,6 +23,7 @@ module Backend end @[GraphQL::Field] + # Creates user def create_user(context : Context, input : UserCreateInput, check_ldap : Bool = true) : User context.admin! @@ -36,6 +38,7 @@ module Backend end @[GraphQL::Field] + # Deletes user by ID def delete_user(context : Context, id : Int32) : Int32 context.admin! @@ -46,6 +49,7 @@ module Backend end @[GraphQL::Field] + # Sends all unregistered teachers a registration email def send_teachers_registration_email(context : Context) : Bool context.admin! @@ -55,6 +59,7 @@ module Backend end @[GraphQL::Field] + # Creates teacher def create_teacher(context : Context, input : TeacherCreateInput) : Teacher context.admin! @@ -63,6 +68,7 @@ module Backend end @[GraphQL::Field] + # Deletes teacher by ID def delete_teacher(context : Context, id : Int32) : Int32 context.admin! @@ -73,6 +79,7 @@ module Backend end @[GraphQL::Field] + # Self register as teacher def register_teacher(context : Context, input : TeacherInput) : Teacher context.teacher? external: false @@ -82,6 +89,7 @@ module Backend end @[GraphQL::Field] + # Creates student def create_student(context : Context, input : StudentCreateInput) : Student context.admin! @@ -93,6 +101,7 @@ module Backend end @[GraphQL::Field] + # Deletes student by ID def delete_student(context : Context, id : Int32) : Int32 context.admin! @@ -103,6 +112,7 @@ module Backend end @[GraphQL::Field] + # Creates vote for authenticated user's student def create_vote(context : Context, input : VoteCreateInput) : Vote context.student! diff --git a/docker/backend/src/backend/api/schema/query.cr b/docker/backend/src/backend/api/schema/query.cr index 2ee95af..7b3a8cc 100644 --- a/docker/backend/src/backend/api/schema/query.cr +++ b/docker/backend/src/backend/api/schema/query.cr @@ -4,11 +4,13 @@ module Backend @[GraphQL::Object] class Query < GraphQL::BaseQuery @[GraphQL::Field] + # Retuns true def ok : Bool true end @[GraphQL::Field] + # Current authenticated user def me(context : Context) : User context.authenticated! @@ -16,6 +18,7 @@ module Backend end @[GraphQL::Field] + # User by ID def user(context : Context, id : Int32) : User context.admin! @@ -23,6 +26,7 @@ module Backend end @[GraphQL::Field] + # All users def users(context : Context) : Array(User) context.admin! @@ -30,6 +34,7 @@ module Backend end @[GraphQL::Field] + # All admins def admins(context : Context) : Array(User) context.admin! @@ -37,16 +42,19 @@ module Backend end @[GraphQL::Field] + # Teacher by ID def teacher(id : Int32) : Teacher Teacher.new(Db::Teacher.find!(id)) end @[GraphQL::Field] + # All teachers def teachers : Array(Teacher) Db::Teacher.all.map { |teacher| Teacher.new(teacher) } end @[GraphQL::Field] + # Student by ID def student(context : Context, id : Int32) : Student context.admin! @@ -54,6 +62,7 @@ module Backend end @[GraphQL::Field] + # All students def students(context : Context) : Array(Student) context.admin! @@ -61,6 +70,7 @@ module Backend end @[GraphQL::Field] + # Vote by ID def vote(context : Context, id : Int32) : Vote context.admin! @@ -68,6 +78,7 @@ module Backend end @[GraphQL::Field] + # All votes def votes(context : Context) : Array(Vote) context.admin! diff --git a/docker/backend/src/backend/api/schema/student.cr b/docker/backend/src/backend/api/schema/student.cr index cf851d2..5c5b6d2 100644 --- a/docker/backend/src/backend/api/schema/student.cr +++ b/docker/backend/src/backend/api/schema/student.cr @@ -2,31 +2,35 @@ module Backend module Api module Schema @[GraphQL::Object] + # Student model class Student < GraphQL::BaseObject include Helpers::DbObject db_object Db::Student @[GraphQL::Field] + # Student's user def user : User User.new(find!.user) end @[GraphQL::Field] + # Student at SKIF def skif : Bool find!.skif end @[GraphQL::Field] + # Student's vote def vote : Vote? - vote = find!.vote - - Vote.new(vote) if vote + find!.vote.try { |v| Vote.new(v) } end end @[GraphQL::InputObject] + # Student base input class StudentInput < GraphQL::BaseInputObject + # Student at SKIF getter skif @[GraphQL::Field] @@ -35,7 +39,9 @@ module Backend end @[GraphQL::InputObject] + # Student creation input class StudentCreateInput < StudentInput + # Student's user ID getter user_id @[GraphQL::Field] diff --git a/docker/backend/src/backend/api/schema/teacher.cr b/docker/backend/src/backend/api/schema/teacher.cr index 5ebc88a..b4e47b2 100644 --- a/docker/backend/src/backend/api/schema/teacher.cr +++ b/docker/backend/src/backend/api/schema/teacher.cr @@ -2,30 +2,38 @@ module Backend module Api module Schema @[GraphQL::Object] + # Teacher model class Teacher < GraphQL::BaseObject include Helpers::DbObject db_object Db::Teacher @[GraphQL::Field] + # Teacher's user def user : User User.new(find!.user) end @[GraphQL::Field] + # Teacher's max students def max_students : Int32 find!.max_students end @[GraphQL::Field] + # Teacher is at SKIF def skif : Bool find!.skif end end @[GraphQL::InputObject] + # Base teacher input class TeacherInput < GraphQL::BaseInputObject + # Teacher's max students getter max_students + + # Teacher at SKIF getter skif @[GraphQL::Field] @@ -34,7 +42,9 @@ module Backend end @[GraphQL::InputObject] + # Teacher creation input class TeacherCreateInput < TeacherInput + # Teacher's user ID getter user_id @[GraphQL::Field] diff --git a/docker/backend/src/backend/api/schema/teacher_vote.cr b/docker/backend/src/backend/api/schema/teacher_vote.cr index cb54697..bcf61e0 100644 --- a/docker/backend/src/backend/api/schema/teacher_vote.cr +++ b/docker/backend/src/backend/api/schema/teacher_vote.cr @@ -2,26 +2,35 @@ module Backend module Api module Schema @[GraphQL::Object] + # Teacher vote model class TeacherVote < GraphQL::BaseObject include Helpers::DbObject db_object Db::TeacherVote @[GraphQL::Field] + # Voted teacher def teacher : Teacher Teacher.new(find!.teacher.not_nil!) end @[GraphQL::Field] + # Teacher vote's priority def priority : Int32 find!.priority end end @[GraphQL::InputObject] + # Teacher vote creation input class TeacherVoteCreateInput < GraphQL::BaseInputObject + # Teacher vote's vote ID getter vote_id + + # Teacher vote's teacher ID getter teacher_id + + # Teacher vote's priority getter priority @[GraphQL::Field] diff --git a/docker/backend/src/backend/api/schema/user.cr b/docker/backend/src/backend/api/schema/user.cr index 017f173..7cd9646 100644 --- a/docker/backend/src/backend/api/schema/user.cr +++ b/docker/backend/src/backend/api/schema/user.cr @@ -2,42 +2,50 @@ module Backend module Api module Schema @[GraphQL::Object] + # User model class User < GraphQL::BaseObject include Helpers::DbObject db_object Db::User @[GraphQL::Field] + # User's first name def firstname : String find!.firstname end @[GraphQL::Field] + # User's last name def lastname : String find!.lastname end @[GraphQL::Field] + # User's full name def name : String find!.name end @[GraphQL::Field] + # User's LDAP username def username : String find!.username end @[GraphQL::Field] + # User's email def email : String find!.email end @[GraphQL::Field] + # User is admin def admin : Bool find!.admin end @[GraphQL::Field] + # User's role def role : UserRole role = Db::UserRole.parse(find!.role) case role @@ -51,6 +59,7 @@ module Backend end @[GraphQL::Field] + # User's external ID def external_id : Int32? case Db::UserRole.parse(find!.role) when .teacher? @@ -63,6 +72,7 @@ module Backend end @[GraphQL::Field] + # User's external teacher object def teacher : Teacher? teacher = find!.teacher if teacher @@ -71,6 +81,7 @@ module Backend end @[GraphQL::Field] + # User's external student object def student : Student? student = find!.student if student @@ -80,6 +91,7 @@ module Backend end @[GraphQL::InputObject] + # User creation input class UserCreateInput < GraphQL::BaseInputObject getter username getter role @@ -93,8 +105,12 @@ module Backend end @[GraphQL::Object] + # Login payload returned after successful login class LoginPayload < GraphQL::BaseObject + # Logged in user property user + + # JWT token property token def initialize( @@ -104,16 +120,19 @@ module Backend end @[GraphQL::Field] + # Logged in user def user : User @user end @[GraphQL::Field] + # Raw bearer token def token : String @token end @[GraphQL::Field] + # Ready to use bearer token def bearer : String Auth::BEARER + @token end diff --git a/docker/backend/src/backend/api/schema/user_role.cr b/docker/backend/src/backend/api/schema/user_role.cr index 3e469b1..f828688 100644 --- a/docker/backend/src/backend/api/schema/user_role.cr +++ b/docker/backend/src/backend/api/schema/user_role.cr @@ -2,6 +2,7 @@ module Backend module Api module Schema @[GraphQL::Enum] + # Possible roles of a user enum UserRole Teacher Student diff --git a/docker/backend/src/backend/api/schema/vote.cr b/docker/backend/src/backend/api/schema/vote.cr index 9a69c31..cd22b8b 100644 --- a/docker/backend/src/backend/api/schema/vote.cr +++ b/docker/backend/src/backend/api/schema/vote.cr @@ -2,24 +2,29 @@ module Backend module Api module Schema @[GraphQL::Object] + # Vote model class Vote < GraphQL::BaseObject include Helpers::DbObject db_object Db::Vote @[GraphQL::Field] + # Student who voted def student : Student Student.new(find!.student) end @[GraphQL::Field] + # Teacher votes for student def teacher_votes : Array(TeacherVote) find!.teacher_votes.map { |tv| TeacherVote.new(tv) } end end @[GraphQL::InputObject] + # Vote creation input class VoteCreateInput < GraphQL::BaseInputObject + # Teacher IDs student votes for getter teacher_ids @[GraphQL::Field] diff --git a/docker/backend/src/backend/api/service.cr b/docker/backend/src/backend/api/service.cr index bc5d9c1..7f1affa 100644 --- a/docker/backend/src/backend/api/service.cr +++ b/docker/backend/src/backend/api/service.cr @@ -1,5 +1,6 @@ module Backend module Api + # Api service SERVICE = ->do Log.info { "Starting Api service..." } run diff --git a/docker/backend/src/backend/api/webserver.cr b/docker/backend/src/backend/api/webserver.cr index 8ce1f5d..ff4d0be 100644 --- a/docker/backend/src/backend/api/webserver.cr +++ b/docker/backend/src/backend/api/webserver.cr @@ -4,11 +4,16 @@ require "json" module Backend module Api + # Api webserver class WebServer include Router + # GraphQL playground HTML code + # + # NOTE: Is minified in production GRAPHQL_PLAYGROUND = {{ flag?(:development) ? read_file("#{__DIR__}/playground.html") : run("./macros/minify_html.cr", read_file("#{__DIR__}/playground.html")).stringify }} + # GraphQL request data serializer struct GraphQLQueryData include JSON::Serializable @@ -17,9 +22,10 @@ module Backend property operation_name : String? end + # "Draws" (creates) routes def draw_routes : Nil # enable graphql playground when in development mode or explicitly enabled - if Backend.config.api.graphql_playground_fully_enabled + if Backend.config.api.graphql_playground_fully_enabled? Log.info { "GraphQL playground enabled" } get "/" do |context| @@ -47,6 +53,7 @@ module Backend end end + # Runs the webserver with according middleware def run : Nil draw_routes diff --git a/docker/backend/src/backend/config.cr b/docker/backend/src/backend/config.cr index fa3dfd2..236111a 100644 --- a/docker/backend/src/backend/config.cr +++ b/docker/backend/src/backend/config.cr @@ -6,90 +6,145 @@ module Backend @@config = Config.new(ENV, prefix: "BACKEND") + # Global configuration def config : Config @@config end + # Environment based configuration class class Config include EnvConfig + # Types of environments program can compiled for / with enum BuildEnv - Development Production + Development end - def development : Bool - {{ flag?(:development) }} - end - - def production : Bool - !development - end - + # Type of environment program is running in def build_env : BuildEnv - development ? BuildEnv::Development : BuildEnv::Production + {{ flag?(:development) }} ? BuildEnv::Development : BuildEnv::Production end + # Production mode + # + # `true` if the build environment is `BuildEnv::Development` + def production? : Bool + build_env.production? + end + + # Development mode + # + # `true` if the build environment is `BuildEnv::Production` + def development? : Bool + build_env.development? + end + + # Base URL of application getter url : String @[EnvConfig::Setting(key: "api")] + # Configuration for `Api` getter api : ApiConfig @[EnvConfig::Setting(key: "worker")] + # Configuration for `Worker` getter worker : WorkerConfig @[EnvConfig::Setting(key: "smtp")] + # Configuration for `Mailers` getter smtp : SmtpConfig @[EnvConfig::Setting(key: "db")] + # Configuration for `Db` getter db : DbConfig @[EnvConfig::Setting(key: "ldap")] + # Configuration for `Ldap` getter ldap : LdapConfig + # Configuration for `Api` class ApiConfig include EnvConfig + # GraphQL playground enable getter graphql_playground : Bool + + # JWT signing key getter jwt_secret : String - def graphql_playground_fully_enabled : Bool - Backend.config.development || graphql_playground + # 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 end end + # Configuration for `Worker` class WorkerConfig include EnvConfig + # Redis URL getter redis_url : String end + # Configuration for `Mailers` class SmtpConfig include EnvConfig + # SMTP host HELO + # + # NOTE: HELOs are [FQDNs](https://en.wikipedia.org/wiki/Fully_qualified_domain_name), so this should be a domain name getter helo : String + + # SMTP hostname getter host : String + + # SMTP port getter port : Int32 + + # Name to send from getter name : String + + # SMTP username getter username : String + + # SMTP password getter password : String end + # Configuration for `Db` class DbConfig include EnvConfig + # Database URL getter url : String end + # Configuration for `Ldap` class LdapConfig include EnvConfig + # LDAP hostname getter host : String + + # LDAP port getter port : Int32 + + # LDAP base DN getter base_dn : String + + # LDAP user base DN + getter base_user_dn : String + + # LDAP bind DN + # + # NOTE: This is the DN to search with getter bind_dn : String + + # LDAP bind password getter bind_password : String - getter user_dn : String end end end diff --git a/docker/backend/src/backend/db.cr b/docker/backend/src/backend/db.cr index 68e2f1e..25f65cf 100644 --- a/docker/backend/src/backend/db.cr +++ b/docker/backend/src/backend/db.cr @@ -4,6 +4,7 @@ require "granite/adapter/pg" require "./db/*" module Backend + # Database model definitions module Db Granite::Connections << Granite::Adapter::Pg.new(name: "pg", url: Backend.config.db.url) end diff --git a/docker/backend/src/backend/db/student.cr b/docker/backend/src/backend/db/student.cr index 97d49a8..f8809a3 100644 --- a/docker/backend/src/backend/db/student.cr +++ b/docker/backend/src/backend/db/student.cr @@ -1,12 +1,16 @@ module Backend module Db + # Student model class Student < Granite::Base table students belongs_to :user has_one :vote + # Student's ID column id : Int64, primary: true + + # Student is at SKIF column skif : Bool end end diff --git a/docker/backend/src/backend/db/teacher.cr b/docker/backend/src/backend/db/teacher.cr index b1516ec..87b4e4d 100644 --- a/docker/backend/src/backend/db/teacher.cr +++ b/docker/backend/src/backend/db/teacher.cr @@ -1,13 +1,19 @@ module Backend module Db + # Teacher model class Teacher < Granite::Base table teachers belongs_to :user has_many teacher_votes : TeacherVote + # Teacher's ID column id : Int64, primary: true + + # Teacher's max students count column max_students : Int32 + + # Teacher is at SKIF column skif : Bool end end diff --git a/docker/backend/src/backend/db/teacher_vote.cr b/docker/backend/src/backend/db/teacher_vote.cr index 8a7a32b..90459fa 100644 --- a/docker/backend/src/backend/db/teacher_vote.cr +++ b/docker/backend/src/backend/db/teacher_vote.cr @@ -1,12 +1,16 @@ module Backend module Db + # Teacher vote model class TeacherVote < Granite::Base table teacher_votes belongs_to :vote belongs_to :teacher + # Teacher votes's ID column id : Int64, primary: true + + # Teacher vote's priority column priority : Int32 validate :teacher, "must be vote unique" do |teacher_vote| diff --git a/docker/backend/src/backend/db/user.cr b/docker/backend/src/backend/db/user.cr index a5ddb2d..013308b 100644 --- a/docker/backend/src/backend/db/user.cr +++ b/docker/backend/src/backend/db/user.cr @@ -1,28 +1,40 @@ module Backend module Db + # User model class User < Granite::Base table users has_one :teacher has_one :student + # User's ID column id : Int64, primary: true + + # User's LDAP username column username : String + + # User's role column role : String + + # User is admin column admin : Bool = false + # User's first name def firstname : String Ldap.user(Ldap.uid(@username.not_nil!)).first["givenName"].first end + # User's last name def lastname : String Ldap.user(Ldap.uid(@username.not_nil!)).first["sn"].first end + # User's full name def name : String Ldap.user(Ldap.uid(@username.not_nil!)).first["cn"].first end + # User's email def email : String Ldap.user(Ldap.uid(@username.not_nil!)).first["mail"].first end diff --git a/docker/backend/src/backend/db/user_role.cr b/docker/backend/src/backend/db/user_role.cr index 1250b63..d847b98 100644 --- a/docker/backend/src/backend/db/user_role.cr +++ b/docker/backend/src/backend/db/user_role.cr @@ -1,5 +1,6 @@ module Backend module Db + # Possible roles a user can have enum UserRole Teacher Student diff --git a/docker/backend/src/backend/ldap.cr b/docker/backend/src/backend/ldap.cr index 74b53f4..6740312 100644 --- a/docker/backend/src/backend/ldap.cr +++ b/docker/backend/src/backend/ldap.cr @@ -3,27 +3,35 @@ require "socket" require "ldap_escape" module Backend + # Provides LDAP utility functions module Ldap extend self + # Creates a new LDAP connection def create_client : LDAP::Client LDAP::Client.new(TCPSocket.new(Backend.config.ldap.host, Backend.config.ldap.port)) end + # Constructs a CN DN from a username def cn(username : String) : String "cn=#{LdapEscape.dn(username)},#{Backend.config.ldap.user_dn}" end + # Constructs a UID DN from a username def uid(uid : String) : String - "uid=#{LdapEscape.dn(uid)},#{Backend.config.ldap.user_dn}" + "uid=#{LdapEscape.dn(uid)},#{Backend.config.ldap.base_user_dn}" end + # Queries the LDAP server for a user + # + # NOTE: Returns a hash of the user's attributes def user(dn : String) : Array(Hash(String, Array(String))) create_client .authenticate(Backend.config.ldap.bind_dn, Backend.config.ldap.bind_password) .search(base: dn) end + # Checks if credentials are valid def authenticate?(dn : String, password : String) : Bool !!create_client.authenticate(dn, password) rescue LDAP::Client::AuthError diff --git a/docker/backend/src/backend/mailers.cr b/docker/backend/src/backend/mailers.cr index 050ca25..2f74881 100644 --- a/docker/backend/src/backend/mailers.cr +++ b/docker/backend/src/backend/mailers.cr @@ -5,6 +5,7 @@ require "kilt" require "./mailers/*" module Backend + # Mailer definitions module Mailers Quartz.config do |config| config.smtp_enabled = true diff --git a/docker/backend/src/backend/mailers/teacher_registration_mailer.cr b/docker/backend/src/backend/mailers/teacher_registration_mailer.cr index 12e8113..4f99dde 100644 --- a/docker/backend/src/backend/mailers/teacher_registration_mailer.cr +++ b/docker/backend/src/backend/mailers/teacher_registration_mailer.cr @@ -1,5 +1,6 @@ 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 diff --git a/docker/backend/src/backend/run.cr b/docker/backend/src/backend/run.cr index 4865f43..3d2d3b9 100644 --- a/docker/backend/src/backend/run.cr +++ b/docker/backend/src/backend/run.cr @@ -1,6 +1,7 @@ module Backend extend self + # Runs backend services def run : Nil {% if flag?(:development) %} Log.warn { "Backend is running in development mode! Do not use this in production!" } diff --git a/docker/backend/src/backend/services.cr b/docker/backend/src/backend/services.cr index 0cbfb1e..43b2421 100644 --- a/docker/backend/src/backend/services.cr +++ b/docker/backend/src/backend/services.cr @@ -1,4 +1,5 @@ module Backend + # Backend services to be included in the application SERVICES = [ Api::SERVICE, Worker::SERVICE, diff --git a/docker/backend/src/backend/worker.cr b/docker/backend/src/backend/worker.cr index 4395346..7b3a1ff 100644 --- a/docker/backend/src/backend/worker.cr +++ b/docker/backend/src/backend/worker.cr @@ -1,8 +1,5 @@ require "mosquito" -module Mosquito::Serializers::Array -end - module Mosquito::Serializers::Granite macro serialize_granite_model(klass) {% method_suffix = klass.resolve.stringify.underscore.gsub(/::/, "__").id %} @@ -21,6 +18,7 @@ end require "./worker/*" module Backend + # Worker module module Worker Mosquito.configure do |settings| settings.redis_url = Backend.config.worker.redis_url diff --git a/docker/backend/src/backend/worker/jobs.cr b/docker/backend/src/backend/worker/jobs.cr index db7012a..df74540 100644 --- a/docker/backend/src/backend/worker/jobs.cr +++ b/docker/backend/src/backend/worker/jobs.cr @@ -1 +1,5 @@ require "./jobs/*" + +# Job definitions +module Backend::Worker::Jobs +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 index 2137bf2..6da07b2 100644 --- 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 @@ -3,7 +3,9 @@ require "../../db/user" 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.where(role: Db::UserRole::Teacher.to_s, teacher_id: nil) count = users.count.run.as(Int64).to_i diff --git a/docker/backend/src/backend/worker/run.cr b/docker/backend/src/backend/worker/run.cr index 6941c91..de57b6c 100644 --- a/docker/backend/src/backend/worker/run.cr +++ b/docker/backend/src/backend/worker/run.cr @@ -2,6 +2,7 @@ module Backend module Worker extend self + # Runs the worker def run : Nil Mosquito::Runner.start end diff --git a/docker/backend/src/backend/worker/service.cr b/docker/backend/src/backend/worker/service.cr index d9cc71e..fc9c0bc 100644 --- a/docker/backend/src/backend/worker/service.cr +++ b/docker/backend/src/backend/worker/service.cr @@ -1,5 +1,6 @@ module Backend module Worker + # Worker service SERVICE = ->do Log.info { "Starting worker service..." } run