Documentation #41

Merged
dergrimm merged 18 commits from documentation into main 2022-02-09 14:48:19 +00:00
40 changed files with 306 additions and 53 deletions

View file

@ -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

View file

@ -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=

View file

@ -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/`.

View file

@ -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:

View file

@ -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

12
docker/backend/Makefile Normal file
View file

@ -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

View file

@ -1 +1,5 @@
require "./backend/*"
# Base module
module Backend
end

View file

@ -1 +1,5 @@
require "./api/*"
# Api module
module Backend::Api
end

View file

@ -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

View file

@ -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

View file

@ -4,6 +4,7 @@ module Backend
module Api
extend self
# Runs API
def run : Nil
WebServer.new.run
end

View file

@ -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

View file

@ -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

View file

@ -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!

View file

@ -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!

View file

@ -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]

View file

@ -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]

View file

@ -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]

View file

@ -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

View file

@ -2,6 +2,7 @@ module Backend
module Api
module Schema
@[GraphQL::Enum]
# Possible roles of a user
enum UserRole
Teacher
Student

View file

@ -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]

View file

@ -1,5 +1,6 @@
module Backend
module Api
# Api service
SERVICE = ->do
Log.info { "Starting Api service..." }
run

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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|

View file

@ -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

View file

@ -1,5 +1,6 @@
module Backend
module Db
# Possible roles a user can have
enum UserRole
Teacher
Student

View file

@ -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

View file

@ -5,6 +5,7 @@ require "kilt"
require "./mailers/*"
module Backend
# Mailer definitions
module Mailers
Quartz.config do |config|
config.smtp_enabled = true

View file

@ -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

View file

@ -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!" }

View file

@ -1,4 +1,5 @@
module Backend
# Backend services to be included in the application
SERVICES = [
Api::SERVICE,
Worker::SERVICE,

View file

@ -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

View file

@ -1 +1,5 @@
require "./jobs/*"
# Job definitions
module Backend::Worker::Jobs
end

View file

@ -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

View file

@ -2,6 +2,7 @@ module Backend
module Worker
extend self
# Runs the worker
def run : Nil
Mosquito::Runner.start
end

View file

@ -1,5 +1,6 @@
module Backend
module Worker
# Worker service
SERVICE = ->do
Log.info { "Starting worker service..." }
run