Merge pull request 'Documentation' (#41) from documentation into main
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #41
This commit is contained in:
commit
ec58832557
29
.drone.yml
29
.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
|
||||
|
|
|
@ -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=
|
||||
|
|
|
@ -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/`.
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
12
docker/backend/Makefile
Normal 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
|
|
@ -1 +1,5 @@
|
|||
require "./backend/*"
|
||||
|
||||
# Base module
|
||||
module Backend
|
||||
end
|
||||
|
|
|
@ -1 +1,5 @@
|
|||
require "./api/*"
|
||||
|
||||
# Api module
|
||||
module Backend::Api
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -4,6 +4,7 @@ module Backend
|
|||
module Api
|
||||
extend self
|
||||
|
||||
# Runs API
|
||||
def run : Nil
|
||||
WebServer.new.run
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!
|
||||
|
||||
|
|
|
@ -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!
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -2,6 +2,7 @@ module Backend
|
|||
module Api
|
||||
module Schema
|
||||
@[GraphQL::Enum]
|
||||
# Possible roles of a user
|
||||
enum UserRole
|
||||
Teacher
|
||||
Student
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
module Backend
|
||||
module Api
|
||||
# Api service
|
||||
SERVICE = ->do
|
||||
Log.info { "Starting Api service..." }
|
||||
run
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
module Backend
|
||||
module Db
|
||||
# Possible roles a user can have
|
||||
enum UserRole
|
||||
Teacher
|
||||
Student
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -5,6 +5,7 @@ require "kilt"
|
|||
require "./mailers/*"
|
||||
|
||||
module Backend
|
||||
# Mailer definitions
|
||||
module Mailers
|
||||
Quartz.config do |config|
|
||||
config.smtp_enabled = true
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!" }
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
module Backend
|
||||
# Backend services to be included in the application
|
||||
SERVICES = [
|
||||
Api::SERVICE,
|
||||
Worker::SERVICE,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1 +1,5 @@
|
|||
require "./jobs/*"
|
||||
|
||||
# Job definitions
|
||||
module Backend::Worker::Jobs
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -2,6 +2,7 @@ module Backend
|
|||
module Worker
|
||||
extend self
|
||||
|
||||
# Runs the worker
|
||||
def run : Nil
|
||||
Mosquito::Runner.start
|
||||
end
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
module Backend
|
||||
module Worker
|
||||
# Worker service
|
||||
SERVICE = ->do
|
||||
Log.info { "Starting worker service..." }
|
||||
run
|
||||
|
|
Loading…
Reference in a new issue