This commit is contained in:
parent
41b27fcfd7
commit
5bc10f8aaf
|
@ -10,7 +10,7 @@ http {
|
|||
# }
|
||||
|
||||
location /graphql {
|
||||
proxy_pass http://api;
|
||||
proxy_pass http://backend;
|
||||
}
|
||||
|
||||
location /adminer {
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
services:
|
||||
nginx:
|
||||
container_name: nginx
|
||||
image: nginx:alpine
|
||||
container_name: nginx
|
||||
restart: always
|
||||
volumes:
|
||||
- ./config/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
ports:
|
||||
- 80:80
|
||||
depends_on:
|
||||
- api
|
||||
- adminer
|
||||
- backend
|
||||
|
||||
db:
|
||||
image: postgres:alpine
|
||||
container_name: db
|
||||
env_file: .env
|
||||
restart: always
|
||||
networks:
|
||||
- db
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
|
@ -22,20 +26,45 @@ services:
|
|||
adminer:
|
||||
image: adminer:standalone
|
||||
container_name: adminer
|
||||
restart: always
|
||||
networks:
|
||||
- default
|
||||
- db
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
api:
|
||||
redis:
|
||||
image: redis:alpine
|
||||
container_name: redis
|
||||
restart: always
|
||||
networks:
|
||||
- redis
|
||||
volumes:
|
||||
- redis:/data
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: ./docker/api
|
||||
context: ./docker/backend
|
||||
args:
|
||||
BUILD_ENV: production
|
||||
container_name: api
|
||||
container_name: backend
|
||||
restart: always
|
||||
networks:
|
||||
- default
|
||||
- db
|
||||
- redis
|
||||
environment:
|
||||
API_DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_USER}
|
||||
API_JWT_SECRET: ${API_JWT_SECRET}
|
||||
BACKEND_DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_USER}
|
||||
BACKEND_JWT_SECRET: ${API_JWT_SECRET}
|
||||
BACKEND_WORKER_REDIS_URL: redis://redis:6379
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
|
||||
networks:
|
||||
db:
|
||||
redis:
|
||||
|
||||
volumes:
|
||||
db: null
|
||||
db:
|
||||
redis:
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
require "crystal-argon2"
|
||||
require "jwt"
|
||||
|
||||
module API
|
||||
module Auth
|
||||
extend self
|
||||
|
||||
BEARER = "Bearer "
|
||||
|
||||
def hash_password(password : String) : String
|
||||
Argon2::Password.create(password)
|
||||
end
|
||||
|
||||
def verify_password?(password : String, hash : String) : Bool
|
||||
!!Argon2::Password.verify_password(password, hash)
|
||||
rescue
|
||||
false
|
||||
end
|
||||
|
||||
private def create_jwt(data, expiration : Int) : String
|
||||
payload = {
|
||||
"data" => data.to_h,
|
||||
"exp" => expiration,
|
||||
}
|
||||
|
||||
JWT.encode(payload.to_h, ENV_REQUESTER["API_JWT_SECRET"], JWT::Algorithm::HS256)
|
||||
end
|
||||
|
||||
def create_user_jwt(user_id : Int, expiration : Int = (Time.utc + Time::Span.new(hours: 6)).to_unix) : String
|
||||
create_jwt({user_id: user_id}, expiration)
|
||||
end
|
||||
|
||||
def decode_jwt(jwt : String) : JSON::Any
|
||||
JWT.decode(jwt, ENV_REQUESTER["API_JWT_SECRET"], JWT::Algorithm::HS256)[0]
|
||||
end
|
||||
|
||||
def decode_jwt?(jwt : String) : JSON::Any?
|
||||
decode_jwt(jwt)
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,81 +0,0 @@
|
|||
require "commander"
|
||||
require "fancyline"
|
||||
|
||||
require "./db"
|
||||
|
||||
module API
|
||||
module CLI
|
||||
extend self
|
||||
|
||||
private FANCY = Fancyline.new
|
||||
|
||||
FANCY.actions.set Fancyline::Key::Control::CtrlC do
|
||||
exit
|
||||
end
|
||||
|
||||
private def input(prompt : String) : String
|
||||
x = FANCY.readline(prompt)
|
||||
|
||||
if x
|
||||
x.chomp.strip
|
||||
else
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
cli = Commander::Command.new do |cmd|
|
||||
cmd.use = "api"
|
||||
cmd.long = "Mentorenwahl API CLI"
|
||||
|
||||
cmd.run do
|
||||
API.run
|
||||
end
|
||||
|
||||
cmd.commands.add do |c|
|
||||
c.use = "seed"
|
||||
c.long = "Seeds the database with required data"
|
||||
|
||||
c.run do
|
||||
puts "Seeding database with admin user..."
|
||||
data = {
|
||||
"firstname" => input("Firstname: "),
|
||||
"lastname" => input("Lastname: "),
|
||||
"email" => input("Email: "),
|
||||
"password" => Auth.hash_password(input("Password: ")),
|
||||
"role" => Db::UserRole::Admin.to_s,
|
||||
}
|
||||
password_confirmation = input("Password confirmation: ")
|
||||
|
||||
if data.values.any?(&.empty?)
|
||||
abort "Values can't be empty!"
|
||||
elsif !Auth.verify_password?(password_confirmation, data["password"])
|
||||
abort "Passwords do not match!"
|
||||
end
|
||||
|
||||
puts "---"
|
||||
data.each { |k, v| puts "#{k.capitalize}: #{v}" }
|
||||
puts "---"
|
||||
|
||||
unless input("Are you sure? (y/N) ").downcase == "y"
|
||||
abort "Aborted!"
|
||||
end
|
||||
|
||||
puts "Seeding database with admin user..."
|
||||
|
||||
user = Db::User.create!(data)
|
||||
admin = Db::Admin.create!(user_id: user.id)
|
||||
|
||||
puts "Done!"
|
||||
|
||||
puts "---"
|
||||
puts "User id: #{user.id}"
|
||||
puts "Admin id: #{admin.id}"
|
||||
puts "Token: #{Auth.create_user_jwt(user_id: user.id.not_nil!)}"
|
||||
puts "---"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Commander.run(cli, ARGV)
|
||||
end
|
||||
end
|
|
@ -1,104 +0,0 @@
|
|||
require "http/request"
|
||||
require "graphql"
|
||||
require "granite"
|
||||
|
||||
module API
|
||||
class Context < GraphQL::Context
|
||||
getter user : Db::User?
|
||||
getter role : Schema::UserRole?
|
||||
getter external : (Db::Admin | Db::Teacher | Db::Student)?
|
||||
|
||||
def initialize(request : HTTP::Request, *rest)
|
||||
super(*rest)
|
||||
|
||||
token = request.headers["Authorization"]?
|
||||
if token && token[..Auth::BEARER.size - 1] == Auth::BEARER
|
||||
payload = Auth.decode_jwt?(token[Auth::BEARER.size..])
|
||||
return unless payload
|
||||
|
||||
data = payload["data"].as_h
|
||||
@user = Db::User.find(data["user_id"].as_i)
|
||||
return if @user.nil? || @user.not_nil!.blocked
|
||||
|
||||
if @user
|
||||
@role = Schema::UserRole.parse?(@user.as(Db::User).role).not_nil!
|
||||
if @role
|
||||
@external =
|
||||
case Schema::UserRole.parse?(@user.not_nil!.role)
|
||||
when Schema::UserRole::Admin
|
||||
@user.not_nil!.admin
|
||||
when Schema::UserRole::Teacher
|
||||
@user.not_nil!.teacher
|
||||
when Schema::UserRole::Student
|
||||
@user.not_nil!.student
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def authenticated? : Bool
|
||||
!@user.nil?
|
||||
end
|
||||
|
||||
def authenticated! : Bool
|
||||
raise "Not authenticated" unless authenticated?
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def role?(external = true, *roles : Schema::UserRole) : Bool
|
||||
return false unless authenticated?
|
||||
|
||||
roles.each do |role|
|
||||
return true if @role == role && if external
|
||||
role == case @external
|
||||
when Db::Admin
|
||||
Schema::UserRole::Admin
|
||||
when Db::Teacher
|
||||
Schema::UserRole::Teacher
|
||||
when Db::Student
|
||||
Schema::UserRole::Student
|
||||
end
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def role!(external = true, *roles : Schema::UserRole) : Bool
|
||||
raise "Invalid permissions" unless role? external, *roles
|
||||
|
||||
true
|
||||
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::Admin, Schema::UserRole::Teacher, Schema::UserRole::Student
|
||||
|
||||
def self.db_eq_role?(external : Granite::Base, role : Schema::UserRole) : Bool
|
||||
role == case external
|
||||
when Db::Admin
|
||||
Schema::UserRole::Admin
|
||||
when Db::Teacher
|
||||
Schema::UserRole::Teacher
|
||||
when Db::Student
|
||||
Schema::UserRole::Student
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,43 +0,0 @@
|
|||
module API
|
||||
class EnvRequester
|
||||
private property keys
|
||||
|
||||
def initialize(@keys = {} of String => String?)
|
||||
end
|
||||
|
||||
def initialize(keys : Array(String))
|
||||
@keys = {} of String => String?
|
||||
keys.each { |k| self.<< k }
|
||||
end
|
||||
|
||||
def <<(key : String) : self
|
||||
@keys[key] = ENV[key]?
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def []?(key : String) : String?
|
||||
if @keys.has_key?(key)
|
||||
@keys[key]?
|
||||
end
|
||||
end
|
||||
|
||||
def [](key : String) : String
|
||||
if @keys.has_key?(key)
|
||||
val = @keys[key]?
|
||||
raise "ENV[#{key}] is nil" unless val
|
||||
|
||||
val
|
||||
else
|
||||
raise "No such key: #{key}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ENV_REQUESTER = EnvRequester.new([
|
||||
"API_DATABASE_URL",
|
||||
"API_ADMIN_EMAIL",
|
||||
"API_ADMIN_PASSWORD",
|
||||
"API_JWT_SECRET",
|
||||
])
|
||||
end
|
|
@ -1,13 +0,0 @@
|
|||
require "http/server"
|
||||
|
||||
module API
|
||||
extend self
|
||||
|
||||
def run : Nil
|
||||
Server.run(80, [HTTP::LogHandler.new, HTTP::ErrorHandler.new]) do |server|
|
||||
address = server.bind_tcp("0.0.0.0", 80, true)
|
||||
puts "Listening on http://#{address}"
|
||||
server.listen
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,10 +0,0 @@
|
|||
require "graphql"
|
||||
|
||||
require "./schema/helpers"
|
||||
require "./schema/*"
|
||||
|
||||
module API
|
||||
module Schema
|
||||
SCHEMA = GraphQL::Schema.new(Query.new, Mutation.new)
|
||||
end
|
||||
end
|
|
@ -1,26 +0,0 @@
|
|||
require "graphql"
|
||||
|
||||
module API
|
||||
module Schema
|
||||
@[GraphQL::Object]
|
||||
class Admin < GraphQL::BaseObject
|
||||
include Helpers::DbObject
|
||||
|
||||
db_object Db::Admin
|
||||
|
||||
@[GraphQL::Field]
|
||||
def user : User
|
||||
User.new(find!.user)
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::InputObject]
|
||||
class AdminCreateInput < GraphQL::BaseInputObject
|
||||
getter user_id
|
||||
|
||||
@[GraphQL::Field]
|
||||
def initialize(@user_id : Int32)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,65 +0,0 @@
|
|||
require "graphql"
|
||||
|
||||
module API
|
||||
module Schema
|
||||
module Helpers
|
||||
module ObjectMacros
|
||||
macro field(type)
|
||||
property {{ type.var }} {% if type.value %} = {{ type.value }}{% end %}
|
||||
|
||||
@[GraphQL::Field]
|
||||
def {{ type.var }} : {{ type.type }}
|
||||
@{{ type.var }}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ObjectDbInit
|
||||
macro db_init(type)
|
||||
def initialize(obj : {{ type }})
|
||||
initialize(obj.id.not_nil!)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ObjectFinders
|
||||
macro finders(type)
|
||||
def find : {{ type }}?
|
||||
{{ type }}.find(@id)
|
||||
end
|
||||
|
||||
def find! : {{ type }}
|
||||
obj = find
|
||||
raise "#{{{ type }}} not found" unless obj
|
||||
|
||||
obj
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module DbObject
|
||||
macro db_object(type)
|
||||
include ::API::Schema::Helpers::ObjectDbInit
|
||||
include ::API::Schema::Helpers::ObjectFinders
|
||||
|
||||
db_init {{ type }}
|
||||
finders {{ type }}
|
||||
|
||||
property id
|
||||
|
||||
def initialize(@id : Int32)
|
||||
end
|
||||
|
||||
def initialize(obj : {{ type }})
|
||||
@id = obj.id.not_nil!.to_i
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def id : Int32
|
||||
@id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,162 +0,0 @@
|
|||
require "graphql"
|
||||
|
||||
module API
|
||||
module Schema
|
||||
@[GraphQL::Object]
|
||||
class Mutation < GraphQL::BaseMutation
|
||||
@[GraphQL::Field]
|
||||
def login(input : LoginInput) : LoginPayload
|
||||
user = Db::User.find_by(email: input.email)
|
||||
raise "Auth failed" unless user && Auth.verify_password?(input.password, user.password)
|
||||
|
||||
LoginPayload.new(
|
||||
user: User.new(user),
|
||||
token: Auth.create_user_jwt(user.id.not_nil!.to_i),
|
||||
)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def update_password(context : Context, password : String) : LoginPayload
|
||||
context.authenticated!
|
||||
|
||||
if Auth.verify_password?(password, context.user.not_nil!.password)
|
||||
raise "New password must be different from old password"
|
||||
end
|
||||
|
||||
context.user.not_nil!.update!(password: Auth.hash_password(password))
|
||||
|
||||
LoginPayload.new(
|
||||
user: User.new(context.user.not_nil!),
|
||||
token: Auth.create_user_jwt(context.user.not_nil!.id.not_nil!.to_i),
|
||||
)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def create_user(context : Context, input : UserCreateInput) : User
|
||||
context.admin!
|
||||
|
||||
user = Db::User.create!(
|
||||
firstname: input.firstname,
|
||||
lastname: input.lastname,
|
||||
email: input.email,
|
||||
password: Auth.hash_password(input.password),
|
||||
role: input.role.to_s,
|
||||
blocked: input.blocked,
|
||||
)
|
||||
if input.create_external && input.role
|
||||
case input.role
|
||||
when UserRole::Teacher
|
||||
user.teacher = Db::Teacher.create!(user_id: user.id, max_students: input.teacher.not_nil!.max_students)
|
||||
when UserRole::Student
|
||||
user.student = Db::Student.create!(user_id: user.id, skif: input.student.not_nil!.skif)
|
||||
end
|
||||
user.save!
|
||||
end
|
||||
|
||||
User.new(user)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def delete_user(context : Context, id : Int32) : Int32
|
||||
context.admin!
|
||||
|
||||
user = Db::User.find!(id)
|
||||
user.destroy!
|
||||
|
||||
id
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def create_admin(context : Context, input : AdminCreateInput) : Admin
|
||||
context.admin!
|
||||
|
||||
admin = Db::Admin.create!(user_id: input.user_id)
|
||||
Admin.new(admin)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def delete_admin(context : Context, id : Int32) : Int32
|
||||
context.admin!
|
||||
|
||||
admin = Db::Admin.find!(id)
|
||||
admin.destroy!
|
||||
|
||||
id
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
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
|
||||
|
||||
@[GraphQL::Field]
|
||||
def delete_teacher(context : Context, id : Int32) : Int32
|
||||
context.admin!
|
||||
|
||||
teacher = Db::Teacher.find!(id)
|
||||
teacher.destroy!
|
||||
|
||||
id
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def register_teacher(context : Context, input : TeacherInput) : Teacher
|
||||
context.teacher? external: false
|
||||
|
||||
Teacher.new(
|
||||
Db::Teacher.create!(user_id: context.user.not_nil!.id, max_students: input.max_students, skif: input.skif)
|
||||
)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def create_student(context : Context, input : StudentCreateInput) : Student
|
||||
context.admin!
|
||||
|
||||
user = Db::User.find!(input.user_id)
|
||||
raise "User not a student" unless UserRole.parse(user.role) == UserRole::Student
|
||||
|
||||
student = Db::Student.create!(user_id: user.id)
|
||||
Student.new(student)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def delete_student(context : Context, id : Int32) : Int32
|
||||
context.admin!
|
||||
|
||||
student = Db::Student.find!(id)
|
||||
student.destroy!
|
||||
|
||||
id
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def create_vote(context : Context, input : VoteCreateInput) : Vote
|
||||
context.student!
|
||||
|
||||
skif = context.external.as(Db::Student).skif
|
||||
input.teacher_ids.each do |id|
|
||||
teacher = Db::Teacher.find(id)
|
||||
|
||||
if teacher.nil?
|
||||
raise "Teachers not found"
|
||||
elsif teacher.skif != skif
|
||||
if teacher.skif
|
||||
raise "Teacher is SKIF, student is not"
|
||||
else
|
||||
raise "Teacher is not SKIF, student is"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
student = context.external.not_nil!.as(Db::Student)
|
||||
vote = Db::Vote.create!(student_id: student.id)
|
||||
Db::TeacherVote.import(input.teacher_ids.map_with_index { |id, i| Db::TeacherVote.new(vote_id: vote.id, teacher_id: id.to_i64, priority: i) })
|
||||
|
||||
Vote.new(vote)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,86 +0,0 @@
|
|||
require "graphql"
|
||||
|
||||
module API
|
||||
module Schema
|
||||
@[GraphQL::Object]
|
||||
class Query < GraphQL::BaseQuery
|
||||
@[GraphQL::Field]
|
||||
def ok : Bool
|
||||
true
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def me(context : Context) : User
|
||||
context.authenticated!
|
||||
|
||||
User.new(context.user.not_nil!)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def user(context : Context, id : Int32) : User
|
||||
context.admin!
|
||||
|
||||
User.new(id)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def users(context : Context) : Array(User)
|
||||
context.admin!
|
||||
|
||||
Db::User.all.map { |user| User.new(user) }
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def admin(context : Context, id : Int32) : Admin
|
||||
context.admin!
|
||||
|
||||
Admin.new(Db::Admin.find!(id))
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def admins(context : Context) : Array(Admin)
|
||||
context.admin!
|
||||
|
||||
Db::Admin.all.map { |admin| Admin.new(admin) }
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def teacher(id : Int32) : Teacher
|
||||
Teacher.new(Db::Teacher.find!(id))
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def teachers : Array(Teacher)
|
||||
Db::Teacher.all.map { |teacher| Teacher.new(teacher) }
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def student(context : Context, id : Int32) : Student
|
||||
context.admin!
|
||||
|
||||
Student.new(Db::Student.find!(id))
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def students(context : Context) : Array(Student)
|
||||
context.admin!
|
||||
|
||||
Db::Student.all.map { |student| Student.new(student) }
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def vote(context : Context, id : Int32) : Vote
|
||||
context.admin!
|
||||
|
||||
Vote.new(Db::Vote.find!(id))
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def votes(context : Context) : Array(Vote)
|
||||
context.admin!
|
||||
|
||||
Db::Vote.all.map { |vote| Vote.new(vote) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,48 +0,0 @@
|
|||
require "graphql"
|
||||
|
||||
module API
|
||||
module Schema
|
||||
@[GraphQL::Object]
|
||||
class Student < GraphQL::BaseObject
|
||||
include Helpers::DbObject
|
||||
|
||||
db_object Db::Student
|
||||
|
||||
@[GraphQL::Field]
|
||||
def user : User
|
||||
User.new(find!.user)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def skif : Bool
|
||||
find!.skif
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def vote : Vote?
|
||||
vote = find!.vote
|
||||
|
||||
Vote.new(vote) if vote
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::InputObject]
|
||||
class StudentInput < GraphQL::BaseInputObject
|
||||
getter skif
|
||||
|
||||
@[GraphQL::Field]
|
||||
def initialize(@skif : Bool)
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::InputObject]
|
||||
class StudentCreateInput < StudentInput
|
||||
getter user_id
|
||||
|
||||
@[GraphQL::Field]
|
||||
def initialize(@user_id : Int32, skif : Bool)
|
||||
super(skif)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,47 +0,0 @@
|
|||
require "graphql"
|
||||
|
||||
module API
|
||||
module Schema
|
||||
@[GraphQL::Object]
|
||||
class Teacher < GraphQL::BaseObject
|
||||
include Helpers::DbObject
|
||||
|
||||
db_object Db::Teacher
|
||||
|
||||
@[GraphQL::Field]
|
||||
def user : User
|
||||
User.new(find!.user)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def max_students : Int32
|
||||
find!.max_students
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def skif : Bool
|
||||
find!.skif
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::InputObject]
|
||||
class TeacherInput < GraphQL::BaseInputObject
|
||||
getter max_students
|
||||
getter skif
|
||||
|
||||
@[GraphQL::Field]
|
||||
def initialize(@max_students : Int32, @skif : Bool)
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::InputObject]
|
||||
class TeacherCreateInput < TeacherInput
|
||||
getter user_id
|
||||
|
||||
@[GraphQL::Field]
|
||||
def initialize(@user_id : Int32, max_students : Int32, skif : Bool)
|
||||
super(max_students, skif)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,33 +0,0 @@
|
|||
require "graphql"
|
||||
|
||||
module API
|
||||
module Schema
|
||||
@[GraphQL::Object]
|
||||
class TeacherVote < GraphQL::BaseObject
|
||||
include Helpers::DbObject
|
||||
|
||||
db_object Db::TeacherVote
|
||||
|
||||
@[GraphQL::Field]
|
||||
def teacher : Teacher
|
||||
Teacher.new(find!.teacher.not_nil!)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def priority : Int32
|
||||
find!.priority
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::InputObject]
|
||||
class TeacherVoteCreateInput < GraphQL::BaseInputObject
|
||||
getter vote_id
|
||||
getter teacher_id
|
||||
getter priority
|
||||
|
||||
@[GraphQL::Field]
|
||||
def initialize(@vote_id : Int32, @teacher_id : Int32, @priority : Int32)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,152 +0,0 @@
|
|||
require "graphql"
|
||||
|
||||
module API
|
||||
module Schema
|
||||
@[GraphQL::Object]
|
||||
class User < GraphQL::BaseObject
|
||||
include Helpers::DbObject
|
||||
|
||||
db_object Db::User
|
||||
|
||||
@[GraphQL::Field]
|
||||
def firstname : String
|
||||
find!.firstname
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def lastname : String
|
||||
find!.lastname
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def email : String
|
||||
find!.email
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def role : UserRole
|
||||
role = Db::UserRole.parse(find!.role)
|
||||
case role
|
||||
when Db::UserRole::Admin
|
||||
UserRole::Admin
|
||||
when Db::UserRole::Teacher
|
||||
UserRole::Teacher
|
||||
when Db::UserRole::Student
|
||||
UserRole::Student
|
||||
else
|
||||
raise "Unknown role: #{role}"
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def external_id : Int32?
|
||||
case Db::UserRole.parse(find!.role)
|
||||
when Db::UserRole::Admin
|
||||
find!.admin
|
||||
when Db::UserRole::Teacher
|
||||
find!.teacher
|
||||
when Db::UserRole::Student
|
||||
find!.student
|
||||
end.not_nil!.id.not_nil!.to_i
|
||||
rescue NilAssertionError
|
||||
nil
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def admin : Admin?
|
||||
admin = find!.admin
|
||||
if admin
|
||||
Admin.new(admin)
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def teacher : Teacher?
|
||||
teacher = find!.teacher
|
||||
if teacher
|
||||
Teacher.new(teacher)
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def student : Student?
|
||||
student = find!.student
|
||||
if student
|
||||
Student.new(student)
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def blocked : Bool
|
||||
find!.blocked
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::InputObject]
|
||||
class UserCreateInput < GraphQL::BaseInputObject
|
||||
getter firstname
|
||||
getter lastname
|
||||
getter email
|
||||
getter password
|
||||
getter role
|
||||
getter teacher
|
||||
getter student
|
||||
getter create_external
|
||||
getter blocked
|
||||
|
||||
@[GraphQL::Field]
|
||||
def initialize(
|
||||
@firstname : String,
|
||||
@lastname : String,
|
||||
@email : String,
|
||||
@password : String,
|
||||
@role : UserRole,
|
||||
@teacher : TeacherInput? = nil,
|
||||
@student : StudentInput? = nil,
|
||||
@create_external : Bool = false,
|
||||
@blocked : Bool = false
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::InputObject]
|
||||
class LoginInput < GraphQL::BaseInputObject
|
||||
getter email
|
||||
getter password
|
||||
|
||||
@[GraphQL::Field]
|
||||
def initialize(
|
||||
@email : String,
|
||||
@password : String
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::Object]
|
||||
class LoginPayload < GraphQL::BaseObject
|
||||
property user
|
||||
property token
|
||||
|
||||
def initialize(
|
||||
@user : User,
|
||||
@token : String
|
||||
)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def user : User
|
||||
@user
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def token : String
|
||||
@token
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def bearer : String
|
||||
Auth::BEARER + @token
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,12 +0,0 @@
|
|||
require "graphql"
|
||||
|
||||
module API
|
||||
module Schema
|
||||
@[GraphQL::Enum]
|
||||
enum UserRole
|
||||
Admin
|
||||
Teacher
|
||||
Student
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,31 +0,0 @@
|
|||
require "graphql"
|
||||
|
||||
module API
|
||||
module Schema
|
||||
@[GraphQL::Object]
|
||||
class Vote < GraphQL::BaseObject
|
||||
include Helpers::DbObject
|
||||
|
||||
db_object Db::Vote
|
||||
|
||||
@[GraphQL::Field]
|
||||
def student : Student
|
||||
Student.new(find!.student)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def teacher_votes : Array(TeacherVote)
|
||||
find!.teacher_votes.map { |tv| TeacherVote.new(tv) }
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::InputObject]
|
||||
class VoteCreateInput < GraphQL::BaseInputObject
|
||||
getter teacher_ids
|
||||
|
||||
@[GraphQL::Field]
|
||||
def initialize(@teacher_ids : Array(Int32))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,31 +0,0 @@
|
|||
require "toro"
|
||||
require "json"
|
||||
|
||||
module API
|
||||
class Server < Toro::Router
|
||||
private struct GraphQLData
|
||||
include JSON::Serializable
|
||||
|
||||
property query : String
|
||||
property variables : Hash(String, JSON::Any)?
|
||||
property operation_name : String?
|
||||
end
|
||||
|
||||
def routes
|
||||
on "graphql" do
|
||||
post do
|
||||
content_type "application/json"
|
||||
|
||||
data = GraphQLData.from_json(context.request.body.not_nil!.gets.not_nil!)
|
||||
|
||||
write Schema::SCHEMA.execute(
|
||||
data.query,
|
||||
data.variables,
|
||||
data.operation_name,
|
||||
Context.new(context.request)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,5 +0,0 @@
|
|||
require "micrate"
|
||||
require "pg"
|
||||
|
||||
Micrate::DB.connection_url = ENV["API_DATABASE_URL"]?
|
||||
Micrate::Cli.run
|
|
@ -17,13 +17,14 @@ RUN if [ "${BUILD_ENV}" = "development" ]; then \
|
|||
fi
|
||||
|
||||
FROM ubuntu:latest as user
|
||||
RUN useradd -u 10001 api
|
||||
RUN useradd -u 10001 backend
|
||||
|
||||
FROM scratch as runner
|
||||
WORKDIR /app
|
||||
COPY --from=user /etc/passwd /etc/passwd
|
||||
COPY --from=builder /app/bin /bin
|
||||
COPY ./db ./db
|
||||
USER api
|
||||
USER backend
|
||||
EXPOSE 80
|
||||
ENTRYPOINT [ "api" ]
|
||||
ENTRYPOINT [ "backend" ]
|
||||
CMD [ "run" ]
|
|
@ -40,6 +40,10 @@ shards:
|
|||
git: https://github.com/graphql-crystal/graphql.git
|
||||
version: 0.3.2+git.commit.8c6dc73c0c898ca511d9d12efefca7c837c25946
|
||||
|
||||
habitat:
|
||||
git: https://github.com/luckyframework/habitat.git
|
||||
version: 0.4.7
|
||||
|
||||
jwt:
|
||||
git: https://github.com/crystal-community/jwt.git
|
||||
version: 1.6.0
|
||||
|
@ -48,6 +52,10 @@ shards:
|
|||
git: https://github.com/juanedi/micrate.git
|
||||
version: 0.12.0
|
||||
|
||||
mosquito:
|
||||
git: https://github.com/mosquito-cr/mosquito.git
|
||||
version: 0.11.1
|
||||
|
||||
openssl_ext:
|
||||
git: https://github.com/spider-gazelle/openssl_ext.git
|
||||
version: 2.1.5
|
||||
|
@ -56,10 +64,26 @@ shards:
|
|||
git: https://github.com/will/crystal-pg.git
|
||||
version: 0.24.0
|
||||
|
||||
pool:
|
||||
git: https://github.com/ysbaddaden/pool.git
|
||||
version: 0.2.4
|
||||
|
||||
redis:
|
||||
git: https://github.com/stefanwille/crystal-redis.git
|
||||
version: 2.8.3
|
||||
|
||||
secrets-env:
|
||||
git: https://github.com/spider-gazelle/secrets-env.git
|
||||
version: 1.3.1
|
||||
|
||||
seg:
|
||||
git: https://github.com/soveran/seg.git
|
||||
version: 0.1.0+git.commit.7f1cee94fb7ed7a2ba15f1388cbaede72a85eef9
|
||||
|
||||
senf:
|
||||
git: https://git.dergrimm.net/dergrimm/senf.git
|
||||
version: 0.1.0
|
||||
|
||||
toro:
|
||||
git: https://github.com/soveran/toro.git
|
||||
version: 0.4.3
|
|
@ -1,12 +1,14 @@
|
|||
name: api
|
||||
name: backend
|
||||
version: 0.1.0
|
||||
|
||||
authors:
|
||||
- Dominic Grimm <dominic.grimm@gmail.com>
|
||||
|
||||
license: MIT
|
||||
|
||||
targets:
|
||||
api:
|
||||
main: src/api.cr
|
||||
backend:
|
||||
main: src/backend.cr
|
||||
micrate:
|
||||
main: src/micrate.cr
|
||||
|
||||
|
@ -35,3 +37,9 @@ dependencies:
|
|||
github: Papierkorb/fancyline
|
||||
micrate:
|
||||
github: juanedi/micrate
|
||||
senf:
|
||||
git: https://git.dergrimm.net/dergrimm/senf.git
|
||||
mosquito:
|
||||
github: mosquito-cr/mosquito
|
||||
secrets-env:
|
||||
github: spider-gazelle/secrets-env
|
7
docker/backend/src/backend.cr
Normal file
7
docker/backend/src/backend.cr
Normal file
|
@ -0,0 +1,7 @@
|
|||
require "secrets-env"
|
||||
|
||||
require "./backend/*"
|
||||
|
||||
module Backend
|
||||
CLI.run
|
||||
end
|
45
docker/backend/src/backend/api/auth.cr
Normal file
45
docker/backend/src/backend/api/auth.cr
Normal file
|
@ -0,0 +1,45 @@
|
|||
require "crystal-argon2"
|
||||
require "jwt"
|
||||
|
||||
module Backend
|
||||
module API
|
||||
module Auth
|
||||
extend self
|
||||
|
||||
BEARER = "Bearer "
|
||||
|
||||
def hash_password(password : String) : String
|
||||
Argon2::Password.create(password)
|
||||
end
|
||||
|
||||
def verify_password?(password : String, hash : String) : Bool
|
||||
!!Argon2::Password.verify_password(password, hash)
|
||||
rescue
|
||||
false
|
||||
end
|
||||
|
||||
private def create_jwt(data, expiration : Int) : String
|
||||
payload = {
|
||||
"data" => data.to_h,
|
||||
"exp" => expiration,
|
||||
}
|
||||
|
||||
JWT.encode(payload.to_h, SAFE_ENV["BACKEND_JWT_SECRET"], JWT::Algorithm::HS256)
|
||||
end
|
||||
|
||||
def create_user_jwt(user_id : Int, expiration : Int = (Time.utc + Time::Span.new(hours: 6)).to_unix) : String
|
||||
create_jwt({user_id: user_id}, expiration)
|
||||
end
|
||||
|
||||
def decode_jwt(jwt : String) : JSON::Any
|
||||
JWT.decode(jwt, SAFE_ENV["BACKEND_JWT_SECRET"], JWT::Algorithm::HS256)[0]
|
||||
end
|
||||
|
||||
def decode_jwt?(jwt : String) : JSON::Any?
|
||||
decode_jwt(jwt)
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
106
docker/backend/src/backend/api/context.cr
Normal file
106
docker/backend/src/backend/api/context.cr
Normal file
|
@ -0,0 +1,106 @@
|
|||
require "http/request"
|
||||
require "graphql"
|
||||
require "granite"
|
||||
|
||||
module Backend
|
||||
module API
|
||||
class Context < GraphQL::Context
|
||||
getter user : Db::User?
|
||||
getter role : Schema::UserRole?
|
||||
getter external : (Db::Admin | Db::Teacher | Db::Student)?
|
||||
|
||||
def initialize(request : HTTP::Request, *rest)
|
||||
super(*rest)
|
||||
|
||||
token = request.headers["Authorization"]?
|
||||
if token && token[..Auth::BEARER.size - 1] == Auth::BEARER
|
||||
payload = Auth.decode_jwt?(token[Auth::BEARER.size..])
|
||||
return unless payload
|
||||
|
||||
data = payload["data"].as_h
|
||||
@user = Db::User.find(data["user_id"].as_i)
|
||||
return if @user.nil? || @user.not_nil!.blocked
|
||||
|
||||
if @user
|
||||
@role = Schema::UserRole.parse?(@user.as(Db::User).role).not_nil!
|
||||
if @role
|
||||
@external =
|
||||
case Schema::UserRole.parse?(@user.not_nil!.role)
|
||||
when Schema::UserRole::Admin
|
||||
@user.not_nil!.admin
|
||||
when Schema::UserRole::Teacher
|
||||
@user.not_nil!.teacher
|
||||
when Schema::UserRole::Student
|
||||
@user.not_nil!.student
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def authenticated? : Bool
|
||||
!@user.nil?
|
||||
end
|
||||
|
||||
def authenticated! : Bool
|
||||
raise "Not authenticated" unless authenticated?
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def role?(external = true, *roles : Schema::UserRole) : Bool
|
||||
return false unless authenticated?
|
||||
|
||||
roles.each do |role|
|
||||
return true if @role == role && if external
|
||||
role == case @external
|
||||
when Db::Admin
|
||||
Schema::UserRole::Admin
|
||||
when Db::Teacher
|
||||
Schema::UserRole::Teacher
|
||||
when Db::Student
|
||||
Schema::UserRole::Student
|
||||
end
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def role!(external = true, *roles : Schema::UserRole) : Bool
|
||||
raise "Invalid permissions" unless role? external, *roles
|
||||
|
||||
true
|
||||
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::Admin, Schema::UserRole::Teacher, Schema::UserRole::Student
|
||||
|
||||
def self.db_eq_role?(external : Granite::Base, role : Schema::UserRole) : Bool
|
||||
role == case external
|
||||
when Db::Admin
|
||||
Schema::UserRole::Admin
|
||||
when Db::Teacher
|
||||
Schema::UserRole::Teacher
|
||||
when Db::Student
|
||||
Schema::UserRole::Student
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
14
docker/backend/src/backend/api/run.cr
Normal file
14
docker/backend/src/backend/api/run.cr
Normal file
|
@ -0,0 +1,14 @@
|
|||
require "http/server"
|
||||
|
||||
module Backend
|
||||
module API
|
||||
extend self
|
||||
|
||||
def run : Nil
|
||||
Server.run(80, [HTTP::LogHandler.new, HTTP::ErrorHandler.new]) do |server|
|
||||
server.bind_tcp("0.0.0.0", 80, true)
|
||||
server.listen
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
12
docker/backend/src/backend/api/schema.cr
Normal file
12
docker/backend/src/backend/api/schema.cr
Normal file
|
@ -0,0 +1,12 @@
|
|||
require "graphql"
|
||||
|
||||
require "./schema/helpers"
|
||||
require "./schema/*"
|
||||
|
||||
module Backend
|
||||
module API
|
||||
module Schema
|
||||
SCHEMA = GraphQL::Schema.new(Query.new, Mutation.new)
|
||||
end
|
||||
end
|
||||
end
|
26
docker/backend/src/backend/api/schema/admin.cr
Normal file
26
docker/backend/src/backend/api/schema/admin.cr
Normal file
|
@ -0,0 +1,26 @@
|
|||
module Backend
|
||||
module API
|
||||
module Schema
|
||||
@[GraphQL::Object]
|
||||
class Admin < GraphQL::BaseObject
|
||||
include Helpers::DbObject
|
||||
|
||||
db_object Db::Admin
|
||||
|
||||
@[GraphQL::Field]
|
||||
def user : User
|
||||
User.new(find!.user)
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::InputObject]
|
||||
class AdminCreateInput < GraphQL::BaseInputObject
|
||||
getter user_id
|
||||
|
||||
@[GraphQL::Field]
|
||||
def initialize(@user_id : Int32)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
65
docker/backend/src/backend/api/schema/helpers.cr
Normal file
65
docker/backend/src/backend/api/schema/helpers.cr
Normal file
|
@ -0,0 +1,65 @@
|
|||
module Backend
|
||||
module API
|
||||
module Schema
|
||||
module Helpers
|
||||
module ObjectMacros
|
||||
macro field(type)
|
||||
property {{ type.var }} {% if type.value %} = {{ type.value }}{% end %}
|
||||
|
||||
@[GraphQL::Field]
|
||||
def {{ type.var }} : {{ type.type }}
|
||||
@{{ type.var }}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ObjectDbInit
|
||||
macro db_init(type)
|
||||
def initialize(obj : {{ type }})
|
||||
initialize(obj.id.not_nil!)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ObjectFinders
|
||||
macro finders(type)
|
||||
def find : {{ type }}?
|
||||
{{ type }}.find(@id)
|
||||
end
|
||||
|
||||
def find! : {{ type }}
|
||||
obj = find
|
||||
raise "#{{{ type }}} not found" unless obj
|
||||
|
||||
obj
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module DbObject
|
||||
macro db_object(type)
|
||||
include ::Backend::API::Schema::Helpers::ObjectDbInit
|
||||
include ::Backend::API::Schema::Helpers::ObjectFinders
|
||||
|
||||
db_init {{ type }}
|
||||
finders {{ type }}
|
||||
|
||||
property id
|
||||
|
||||
def initialize(@id : Int32)
|
||||
end
|
||||
|
||||
def initialize(obj : {{ type }})
|
||||
@id = obj.id.not_nil!.to_i
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def id : Int32
|
||||
@id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
162
docker/backend/src/backend/api/schema/mutation.cr
Normal file
162
docker/backend/src/backend/api/schema/mutation.cr
Normal file
|
@ -0,0 +1,162 @@
|
|||
module Backend
|
||||
module API
|
||||
module Schema
|
||||
@[GraphQL::Object]
|
||||
class Mutation < GraphQL::BaseMutation
|
||||
@[GraphQL::Field]
|
||||
def login(input : LoginInput) : LoginPayload
|
||||
user = Db::User.find_by(email: input.email)
|
||||
raise "Auth failed" unless user && Auth.verify_password?(input.password, user.password)
|
||||
|
||||
LoginPayload.new(
|
||||
user: User.new(user),
|
||||
token: Auth.create_user_jwt(user.id.not_nil!.to_i),
|
||||
)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def update_password(context : Context, password : String) : LoginPayload
|
||||
context.authenticated!
|
||||
|
||||
if Auth.verify_password?(password, context.user.not_nil!.password)
|
||||
raise "New password must be different from old password"
|
||||
end
|
||||
|
||||
context.user.not_nil!.update!(password: Auth.hash_password(password))
|
||||
|
||||
LoginPayload.new(
|
||||
user: User.new(context.user.not_nil!),
|
||||
token: Auth.create_user_jwt(context.user.not_nil!.id.not_nil!.to_i),
|
||||
)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def create_user(context : Context, input : UserCreateInput) : User
|
||||
context.admin!
|
||||
|
||||
user = Db::User.create!(
|
||||
firstname: input.firstname,
|
||||
lastname: input.lastname,
|
||||
email: input.email,
|
||||
password: Auth.hash_password(input.password),
|
||||
role: input.role.to_s,
|
||||
blocked: input.blocked,
|
||||
)
|
||||
if input.create_external && input.role
|
||||
case input.role
|
||||
when UserRole::Teacher
|
||||
user.teacher = Db::Teacher.create!(user_id: user.id, max_students: input.teacher.not_nil!.max_students)
|
||||
when UserRole::Student
|
||||
user.student = Db::Student.create!(user_id: user.id, skif: input.student.not_nil!.skif)
|
||||
end
|
||||
user.save!
|
||||
end
|
||||
|
||||
User.new(user)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def delete_user(context : Context, id : Int32) : Int32
|
||||
context.admin!
|
||||
|
||||
user = Db::User.find!(id)
|
||||
user.destroy!
|
||||
|
||||
id
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def create_admin(context : Context, input : AdminCreateInput) : Admin
|
||||
context.admin!
|
||||
|
||||
admin = Db::Admin.create!(user_id: input.user_id)
|
||||
Admin.new(admin)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def delete_admin(context : Context, id : Int32) : Int32
|
||||
context.admin!
|
||||
|
||||
admin = Db::Admin.find!(id)
|
||||
admin.destroy!
|
||||
|
||||
id
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
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
|
||||
|
||||
@[GraphQL::Field]
|
||||
def delete_teacher(context : Context, id : Int32) : Int32
|
||||
context.admin!
|
||||
|
||||
teacher = Db::Teacher.find!(id)
|
||||
teacher.destroy!
|
||||
|
||||
id
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def register_teacher(context : Context, input : TeacherInput) : Teacher
|
||||
context.teacher? external: false
|
||||
|
||||
Teacher.new(
|
||||
Db::Teacher.create!(user_id: context.user.not_nil!.id, max_students: input.max_students, skif: input.skif)
|
||||
)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def create_student(context : Context, input : StudentCreateInput) : Student
|
||||
context.admin!
|
||||
|
||||
user = Db::User.find!(input.user_id)
|
||||
raise "User not a student" unless UserRole.parse(user.role) == UserRole::Student
|
||||
|
||||
student = Db::Student.create!(user_id: user.id)
|
||||
Student.new(student)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def delete_student(context : Context, id : Int32) : Int32
|
||||
context.admin!
|
||||
|
||||
student = Db::Student.find!(id)
|
||||
student.destroy!
|
||||
|
||||
id
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def create_vote(context : Context, input : VoteCreateInput) : Vote
|
||||
context.student!
|
||||
|
||||
skif = context.external.as(Db::Student).skif
|
||||
input.teacher_ids.each do |id|
|
||||
teacher = Db::Teacher.find(id)
|
||||
|
||||
if teacher.nil?
|
||||
raise "Teachers not found"
|
||||
elsif teacher.skif != skif
|
||||
if teacher.skif
|
||||
raise "Teacher is SKIF, student is not"
|
||||
else
|
||||
raise "Teacher is not SKIF, student is"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
student = context.external.not_nil!.as(Db::Student)
|
||||
vote = Db::Vote.create!(student_id: student.id)
|
||||
Db::TeacherVote.import(input.teacher_ids.map_with_index { |id, i| Db::TeacherVote.new(vote_id: vote.id, teacher_id: id.to_i64, priority: i) })
|
||||
|
||||
Vote.new(vote)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
86
docker/backend/src/backend/api/schema/query.cr
Normal file
86
docker/backend/src/backend/api/schema/query.cr
Normal file
|
@ -0,0 +1,86 @@
|
|||
module Backend
|
||||
module API
|
||||
module Schema
|
||||
@[GraphQL::Object]
|
||||
class Query < GraphQL::BaseQuery
|
||||
@[GraphQL::Field]
|
||||
def ok : Bool
|
||||
true
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def me(context : Context) : User
|
||||
context.authenticated!
|
||||
|
||||
User.new(context.user.not_nil!)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def user(context : Context, id : Int32) : User
|
||||
context.admin!
|
||||
|
||||
User.new(id)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def users(context : Context) : Array(User)
|
||||
context.admin!
|
||||
|
||||
Db::User.all.map { |user| User.new(user) }
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def admin(context : Context, id : Int32) : Admin
|
||||
context.admin!
|
||||
|
||||
Admin.new(Db::Admin.find!(id))
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def admins(context : Context) : Array(Admin)
|
||||
context.admin!
|
||||
|
||||
Db::Admin.all.map { |admin| Admin.new(admin) }
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def teacher(id : Int32) : Teacher
|
||||
Teacher.new(Db::Teacher.find!(id))
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def teachers : Array(Teacher)
|
||||
Db::Teacher.all.map { |teacher| Teacher.new(teacher) }
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def student(context : Context, id : Int32) : Student
|
||||
context.admin!
|
||||
|
||||
Student.new(Db::Student.find!(id))
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def students(context : Context) : Array(Student)
|
||||
context.admin!
|
||||
|
||||
Db::Student.all.map { |student| Student.new(student) }
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def vote(context : Context, id : Int32) : Vote
|
||||
context.admin!
|
||||
|
||||
Vote.new(Db::Vote.find!(id))
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def votes(context : Context) : Array(Vote)
|
||||
context.admin!
|
||||
|
||||
Db::Vote.all.map { |vote| Vote.new(vote) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
48
docker/backend/src/backend/api/schema/student.cr
Normal file
48
docker/backend/src/backend/api/schema/student.cr
Normal file
|
@ -0,0 +1,48 @@
|
|||
module Backend
|
||||
module API
|
||||
module Schema
|
||||
@[GraphQL::Object]
|
||||
class Student < GraphQL::BaseObject
|
||||
include Helpers::DbObject
|
||||
|
||||
db_object Db::Student
|
||||
|
||||
@[GraphQL::Field]
|
||||
def user : User
|
||||
User.new(find!.user)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def skif : Bool
|
||||
find!.skif
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def vote : Vote?
|
||||
vote = find!.vote
|
||||
|
||||
Vote.new(vote) if vote
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::InputObject]
|
||||
class StudentInput < GraphQL::BaseInputObject
|
||||
getter skif
|
||||
|
||||
@[GraphQL::Field]
|
||||
def initialize(@skif : Bool)
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::InputObject]
|
||||
class StudentCreateInput < StudentInput
|
||||
getter user_id
|
||||
|
||||
@[GraphQL::Field]
|
||||
def initialize(@user_id : Int32, skif : Bool)
|
||||
super(skif)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
47
docker/backend/src/backend/api/schema/teacher.cr
Normal file
47
docker/backend/src/backend/api/schema/teacher.cr
Normal file
|
@ -0,0 +1,47 @@
|
|||
module Backend
|
||||
module API
|
||||
module Schema
|
||||
@[GraphQL::Object]
|
||||
class Teacher < GraphQL::BaseObject
|
||||
include Helpers::DbObject
|
||||
|
||||
db_object Db::Teacher
|
||||
|
||||
@[GraphQL::Field]
|
||||
def user : User
|
||||
User.new(find!.user)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def max_students : Int32
|
||||
find!.max_students
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def skif : Bool
|
||||
find!.skif
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::InputObject]
|
||||
class TeacherInput < GraphQL::BaseInputObject
|
||||
getter max_students
|
||||
getter skif
|
||||
|
||||
@[GraphQL::Field]
|
||||
def initialize(@max_students : Int32, @skif : Bool)
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::InputObject]
|
||||
class TeacherCreateInput < TeacherInput
|
||||
getter user_id
|
||||
|
||||
@[GraphQL::Field]
|
||||
def initialize(@user_id : Int32, max_students : Int32, skif : Bool)
|
||||
super(max_students, skif)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
33
docker/backend/src/backend/api/schema/teacher_vote.cr
Normal file
33
docker/backend/src/backend/api/schema/teacher_vote.cr
Normal file
|
@ -0,0 +1,33 @@
|
|||
module Backend
|
||||
module API
|
||||
module Schema
|
||||
@[GraphQL::Object]
|
||||
class TeacherVote < GraphQL::BaseObject
|
||||
include Helpers::DbObject
|
||||
|
||||
db_object Db::TeacherVote
|
||||
|
||||
@[GraphQL::Field]
|
||||
def teacher : Teacher
|
||||
Teacher.new(find!.teacher.not_nil!)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def priority : Int32
|
||||
find!.priority
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::InputObject]
|
||||
class TeacherVoteCreateInput < GraphQL::BaseInputObject
|
||||
getter vote_id
|
||||
getter teacher_id
|
||||
getter priority
|
||||
|
||||
@[GraphQL::Field]
|
||||
def initialize(@vote_id : Int32, @teacher_id : Int32, @priority : Int32)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
152
docker/backend/src/backend/api/schema/user.cr
Normal file
152
docker/backend/src/backend/api/schema/user.cr
Normal file
|
@ -0,0 +1,152 @@
|
|||
module Backend
|
||||
module API
|
||||
module Schema
|
||||
@[GraphQL::Object]
|
||||
class User < GraphQL::BaseObject
|
||||
include Helpers::DbObject
|
||||
|
||||
db_object Db::User
|
||||
|
||||
@[GraphQL::Field]
|
||||
def firstname : String
|
||||
find!.firstname
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def lastname : String
|
||||
find!.lastname
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def email : String
|
||||
find!.email
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def role : UserRole
|
||||
role = Db::UserRole.parse(find!.role)
|
||||
case role
|
||||
when Db::UserRole::Admin
|
||||
UserRole::Admin
|
||||
when Db::UserRole::Teacher
|
||||
UserRole::Teacher
|
||||
when Db::UserRole::Student
|
||||
UserRole::Student
|
||||
else
|
||||
raise "Unknown role: #{role}"
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def external_id : Int32?
|
||||
case Db::UserRole.parse(find!.role)
|
||||
when Db::UserRole::Admin
|
||||
find!.admin
|
||||
when Db::UserRole::Teacher
|
||||
find!.teacher
|
||||
when Db::UserRole::Student
|
||||
find!.student
|
||||
end.not_nil!.id.not_nil!.to_i
|
||||
rescue NilAssertionError
|
||||
nil
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def admin : Admin?
|
||||
admin = find!.admin
|
||||
if admin
|
||||
Admin.new(admin)
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def teacher : Teacher?
|
||||
teacher = find!.teacher
|
||||
if teacher
|
||||
Teacher.new(teacher)
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def student : Student?
|
||||
student = find!.student
|
||||
if student
|
||||
Student.new(student)
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def blocked : Bool
|
||||
find!.blocked
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::InputObject]
|
||||
class UserCreateInput < GraphQL::BaseInputObject
|
||||
getter firstname
|
||||
getter lastname
|
||||
getter email
|
||||
getter password
|
||||
getter role
|
||||
getter teacher
|
||||
getter student
|
||||
getter create_external
|
||||
getter blocked
|
||||
|
||||
@[GraphQL::Field]
|
||||
def initialize(
|
||||
@firstname : String,
|
||||
@lastname : String,
|
||||
@email : String,
|
||||
@password : String,
|
||||
@role : UserRole,
|
||||
@teacher : TeacherInput? = nil,
|
||||
@student : StudentInput? = nil,
|
||||
@create_external : Bool = false,
|
||||
@blocked : Bool = false
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::InputObject]
|
||||
class LoginInput < GraphQL::BaseInputObject
|
||||
getter email
|
||||
getter password
|
||||
|
||||
@[GraphQL::Field]
|
||||
def initialize(
|
||||
@email : String,
|
||||
@password : String
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::Object]
|
||||
class LoginPayload < GraphQL::BaseObject
|
||||
property user
|
||||
property token
|
||||
|
||||
def initialize(
|
||||
@user : User,
|
||||
@token : String
|
||||
)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def user : User
|
||||
@user
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def token : String
|
||||
@token
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def bearer : String
|
||||
Auth::BEARER + @token
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
12
docker/backend/src/backend/api/schema/user_role.cr
Normal file
12
docker/backend/src/backend/api/schema/user_role.cr
Normal file
|
@ -0,0 +1,12 @@
|
|||
module Backend
|
||||
module API
|
||||
module Schema
|
||||
@[GraphQL::Enum]
|
||||
enum UserRole
|
||||
Admin
|
||||
Teacher
|
||||
Student
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
31
docker/backend/src/backend/api/schema/vote.cr
Normal file
31
docker/backend/src/backend/api/schema/vote.cr
Normal file
|
@ -0,0 +1,31 @@
|
|||
module Backend
|
||||
module API
|
||||
module Schema
|
||||
@[GraphQL::Object]
|
||||
class Vote < GraphQL::BaseObject
|
||||
include Helpers::DbObject
|
||||
|
||||
db_object Db::Vote
|
||||
|
||||
@[GraphQL::Field]
|
||||
def student : Student
|
||||
Student.new(find!.student)
|
||||
end
|
||||
|
||||
@[GraphQL::Field]
|
||||
def teacher_votes : Array(TeacherVote)
|
||||
find!.teacher_votes.map { |tv| TeacherVote.new(tv) }
|
||||
end
|
||||
end
|
||||
|
||||
@[GraphQL::InputObject]
|
||||
class VoteCreateInput < GraphQL::BaseInputObject
|
||||
getter teacher_ids
|
||||
|
||||
@[GraphQL::Field]
|
||||
def initialize(@teacher_ids : Array(Int32))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
33
docker/backend/src/backend/api/server.cr
Normal file
33
docker/backend/src/backend/api/server.cr
Normal file
|
@ -0,0 +1,33 @@
|
|||
require "toro"
|
||||
require "json"
|
||||
|
||||
module Backend
|
||||
module API
|
||||
class Server < Toro::Router
|
||||
private struct GraphQLData
|
||||
include JSON::Serializable
|
||||
|
||||
property query : String
|
||||
property variables : Hash(String, JSON::Any)?
|
||||
property operation_name : String?
|
||||
end
|
||||
|
||||
def routes
|
||||
on "graphql" do
|
||||
post do
|
||||
content_type "application/json"
|
||||
|
||||
data = GraphQLData.from_json(context.request.body.not_nil!.gets.not_nil!)
|
||||
|
||||
write Schema::SCHEMA.execute(
|
||||
data.query,
|
||||
data.variables,
|
||||
data.operation_name,
|
||||
Context.new(context.request)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
92
docker/backend/src/backend/cli.cr
Normal file
92
docker/backend/src/backend/cli.cr
Normal file
|
@ -0,0 +1,92 @@
|
|||
require "commander"
|
||||
require "fancyline"
|
||||
|
||||
require "./db"
|
||||
|
||||
module Backend
|
||||
module CLI
|
||||
extend self
|
||||
|
||||
private FANCY = Fancyline.new
|
||||
|
||||
private def input(prompt : String) : String
|
||||
x = FANCY.readline(prompt)
|
||||
|
||||
if x
|
||||
x.chomp.strip
|
||||
else
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
def run : Nil
|
||||
FANCY.actions.set Fancyline::Key::Control::CtrlC do
|
||||
exit
|
||||
end
|
||||
|
||||
cli = Commander::Command.new do |cmd|
|
||||
cmd.use = "backend"
|
||||
cmd.long = "Mentorenwahl backend CLI"
|
||||
|
||||
cmd.run do
|
||||
puts cmd.help
|
||||
end
|
||||
|
||||
cmd.commands.add do |c|
|
||||
c.use = "run"
|
||||
c.long = "Run the backend"
|
||||
|
||||
c.run do
|
||||
Backend.run
|
||||
end
|
||||
end
|
||||
|
||||
cmd.commands.add do |c|
|
||||
c.use = "seed"
|
||||
c.long = "Seeds the database with required data"
|
||||
|
||||
c.run do
|
||||
puts "Seeding database with admin user..."
|
||||
data = {
|
||||
"firstname" => input("Firstname: "),
|
||||
"lastname" => input("Lastname: "),
|
||||
"email" => input("Email: "),
|
||||
"password" => API::Auth.hash_password(input("Password: ")),
|
||||
"role" => Db::UserRole::Admin.to_s,
|
||||
}
|
||||
password_confirmation = input("Password confirmation: ")
|
||||
|
||||
if data.values.any?(&.empty?)
|
||||
abort "Values can't be empty!"
|
||||
elsif !API::Auth.verify_password?(password_confirmation, data["password"])
|
||||
abort "Passwords do not match!"
|
||||
end
|
||||
|
||||
puts "---"
|
||||
data.each { |k, v| puts "#{k.capitalize}: #{v}" }
|
||||
puts "---"
|
||||
|
||||
unless input("Are you sure? (y/N) ").downcase == "y"
|
||||
abort "Aborted!"
|
||||
end
|
||||
|
||||
puts "Seeding database with admin user..."
|
||||
|
||||
user = Db::User.create!(data)
|
||||
admin = Db::Admin.create!(user_id: user.id)
|
||||
|
||||
puts "Done!"
|
||||
|
||||
puts "---"
|
||||
puts "User id: #{user.id}"
|
||||
puts "Admin id: #{admin.id}"
|
||||
puts "Token: #{API::Auth.create_user_jwt(user_id: user.id.not_nil!)}"
|
||||
puts "---"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Commander.run(cli, ARGV)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,9 +1,10 @@
|
|||
require "granite"
|
||||
require "granite/adapter/pg"
|
||||
|
||||
require "./db/*"
|
||||
|
||||
module API
|
||||
module Backend
|
||||
module Db
|
||||
Granite::Connections << Granite::Adapter::Pg.new(name: "pg", url: ENV_REQUESTER["API_DATABASE_URL"])
|
||||
Granite::Connections << Granite::Adapter::Pg.new(name: "pg", url: SAFE_ENV["BACKEND_DATABASE_URL"])
|
||||
end
|
||||
end
|
|
@ -1,6 +1,4 @@
|
|||
require "granite"
|
||||
|
||||
module API
|
||||
module Backend
|
||||
module Db
|
||||
class Admin < Granite::Base
|
||||
table admins
|
|
@ -1,6 +1,4 @@
|
|||
require "granite"
|
||||
|
||||
module API
|
||||
module Backend
|
||||
module Db
|
||||
class Student < Granite::Base
|
||||
table students
|
|
@ -1,6 +1,4 @@
|
|||
require "granite"
|
||||
|
||||
module API
|
||||
module Backend
|
||||
module Db
|
||||
class Teacher < Granite::Base
|
||||
table teachers
|
|
@ -1,6 +1,4 @@
|
|||
require "granite"
|
||||
|
||||
module API
|
||||
module Backend
|
||||
module Db
|
||||
class TeacherVote < Granite::Base
|
||||
table teacher_votes
|
|
@ -1,7 +1,6 @@
|
|||
require "CrystalEmail"
|
||||
require "granite"
|
||||
|
||||
module API
|
||||
module Backend
|
||||
module Db
|
||||
class User < Granite::Base
|
||||
table users
|
|
@ -1,4 +1,4 @@
|
|||
module API
|
||||
module Backend
|
||||
module Db
|
||||
enum UserRole
|
||||
Admin
|
|
@ -1,6 +1,4 @@
|
|||
require "granite"
|
||||
|
||||
module API
|
||||
module Backend
|
||||
module Db
|
||||
class Vote < Granite::Base
|
||||
table votes
|
28
docker/backend/src/backend/run.cr
Normal file
28
docker/backend/src/backend/run.cr
Normal file
|
@ -0,0 +1,28 @@
|
|||
module Backend
|
||||
extend self
|
||||
|
||||
def run : Nil
|
||||
puts "Running backend..."
|
||||
puts "-" * 10
|
||||
|
||||
channel = Channel(Nil).new
|
||||
|
||||
spawn same_thread: true do
|
||||
puts "Starting API..."
|
||||
API.run
|
||||
|
||||
channel.send(nil)
|
||||
end
|
||||
|
||||
spawn same_thread: true do
|
||||
puts "Starting worker..."
|
||||
Worker.run
|
||||
|
||||
channel.send(nil)
|
||||
end
|
||||
|
||||
2.times do
|
||||
channel.receive
|
||||
end
|
||||
end
|
||||
end
|
11
docker/backend/src/backend/safe_env.cr
Normal file
11
docker/backend/src/backend/safe_env.cr
Normal file
|
@ -0,0 +1,11 @@
|
|||
require "senf"
|
||||
|
||||
module Backend
|
||||
SAFE_ENV = Senf::SafeEnv.new([
|
||||
"BACKEND_DATABASE_URL",
|
||||
"BACKEND_ADMIN_EMAIL",
|
||||
"BACKEND_ADMIN_PASSWORD",
|
||||
"BACKEND_JWT_SECRET",
|
||||
"BACKEND_WORKER_REDIS_URL",
|
||||
])
|
||||
end
|
11
docker/backend/src/backend/worker.cr
Normal file
11
docker/backend/src/backend/worker.cr
Normal file
|
@ -0,0 +1,11 @@
|
|||
require "mosquito"
|
||||
|
||||
require "./worker/*"
|
||||
|
||||
module Backend
|
||||
module Worker
|
||||
Mosquito.configure do |settings|
|
||||
settings.redis_url = SAFE_ENV["BACKEND_WORKER_REDIS_URL"]
|
||||
end
|
||||
end
|
||||
end
|
1
docker/backend/src/backend/worker/jobs.cr
Normal file
1
docker/backend/src/backend/worker/jobs.cr
Normal file
|
@ -0,0 +1 @@
|
|||
require "./jobs/*"
|
13
docker/backend/src/backend/worker/jobs/hello_world.cr
Normal file
13
docker/backend/src/backend/worker/jobs/hello_world.cr
Normal file
|
@ -0,0 +1,13 @@
|
|||
module Backend
|
||||
module Worker
|
||||
module Jobs
|
||||
class HelloWorldJob < Mosquito::PeriodicJob
|
||||
run_every 30.seconds
|
||||
|
||||
def perform : Nil
|
||||
log "Hello World!"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
9
docker/backend/src/backend/worker/run.cr
Normal file
9
docker/backend/src/backend/worker/run.cr
Normal file
|
@ -0,0 +1,9 @@
|
|||
module Backend
|
||||
module Worker
|
||||
extend self
|
||||
|
||||
def run : Nil
|
||||
Mosquito::Runner.start
|
||||
end
|
||||
end
|
||||
end
|
11
docker/backend/src/micrate.cr
Normal file
11
docker/backend/src/micrate.cr
Normal file
|
@ -0,0 +1,11 @@
|
|||
require "secrets-env"
|
||||
require "senf"
|
||||
require "micrate"
|
||||
require "pg"
|
||||
|
||||
SAFE_ENV = Senf::SafeEnv.new([
|
||||
"BACKEND_DATABASE_URL",
|
||||
])
|
||||
|
||||
Micrate::DB.connection_url = SAFE_ENV["BACKEND_DATABASE_URL"]?
|
||||
Micrate::Cli.run
|
3
scripts/backend.sh
Normal file
3
scripts/backend.sh
Normal file
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
docker-compose exec backend backend "$@"
|
|
@ -1,3 +1,3 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
docker-compose exec api micrate "$@"
|
||||
docker-compose exec backend micrate "$@"
|
||||
|
|
Loading…
Reference in a new issue