This commit is contained in:
parent
41b27fcfd7
commit
5bc10f8aaf
|
@ -10,7 +10,7 @@ http {
|
||||||
# }
|
# }
|
||||||
|
|
||||||
location /graphql {
|
location /graphql {
|
||||||
proxy_pass http://api;
|
proxy_pass http://backend;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /adminer {
|
location /adminer {
|
||||||
|
|
|
@ -1,18 +1,22 @@
|
||||||
services:
|
services:
|
||||||
nginx:
|
nginx:
|
||||||
container_name: nginx
|
|
||||||
image: nginx:alpine
|
image: nginx:alpine
|
||||||
|
container_name: nginx
|
||||||
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- ./config/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
- ./config/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
ports:
|
ports:
|
||||||
- 80:80
|
- 80:80
|
||||||
depends_on:
|
depends_on:
|
||||||
- api
|
- adminer
|
||||||
|
- backend
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:alpine
|
image: postgres:alpine
|
||||||
container_name: db
|
container_name: db
|
||||||
env_file: .env
|
restart: always
|
||||||
|
networks:
|
||||||
|
- db
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: ${POSTGRES_USER}
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
@ -22,20 +26,45 @@ services:
|
||||||
adminer:
|
adminer:
|
||||||
image: adminer:standalone
|
image: adminer:standalone
|
||||||
container_name: adminer
|
container_name: adminer
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
- db
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
|
|
||||||
api:
|
redis:
|
||||||
|
image: redis:alpine
|
||||||
|
container_name: redis
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- redis
|
||||||
|
volumes:
|
||||||
|
- redis:/data
|
||||||
|
|
||||||
|
backend:
|
||||||
build:
|
build:
|
||||||
context: ./docker/api
|
context: ./docker/backend
|
||||||
args:
|
args:
|
||||||
BUILD_ENV: production
|
BUILD_ENV: production
|
||||||
container_name: api
|
container_name: backend
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
- db
|
||||||
|
- redis
|
||||||
environment:
|
environment:
|
||||||
API_DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_USER}
|
BACKEND_DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_USER}
|
||||||
API_JWT_SECRET: ${API_JWT_SECRET}
|
BACKEND_JWT_SECRET: ${API_JWT_SECRET}
|
||||||
|
BACKEND_WORKER_REDIS_URL: redis://redis:6379
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
|
- redis
|
||||||
|
|
||||||
|
networks:
|
||||||
|
db:
|
||||||
|
redis:
|
||||||
|
|
||||||
volumes:
|
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
|
fi
|
||||||
|
|
||||||
FROM ubuntu:latest as user
|
FROM ubuntu:latest as user
|
||||||
RUN useradd -u 10001 api
|
RUN useradd -u 10001 backend
|
||||||
|
|
||||||
FROM scratch as runner
|
FROM scratch as runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=user /etc/passwd /etc/passwd
|
COPY --from=user /etc/passwd /etc/passwd
|
||||||
COPY --from=builder /app/bin /bin
|
COPY --from=builder /app/bin /bin
|
||||||
COPY ./db ./db
|
COPY ./db ./db
|
||||||
USER api
|
USER backend
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
ENTRYPOINT [ "api" ]
|
ENTRYPOINT [ "backend" ]
|
||||||
|
CMD [ "run" ]
|
|
@ -40,6 +40,10 @@ shards:
|
||||||
git: https://github.com/graphql-crystal/graphql.git
|
git: https://github.com/graphql-crystal/graphql.git
|
||||||
version: 0.3.2+git.commit.8c6dc73c0c898ca511d9d12efefca7c837c25946
|
version: 0.3.2+git.commit.8c6dc73c0c898ca511d9d12efefca7c837c25946
|
||||||
|
|
||||||
|
habitat:
|
||||||
|
git: https://github.com/luckyframework/habitat.git
|
||||||
|
version: 0.4.7
|
||||||
|
|
||||||
jwt:
|
jwt:
|
||||||
git: https://github.com/crystal-community/jwt.git
|
git: https://github.com/crystal-community/jwt.git
|
||||||
version: 1.6.0
|
version: 1.6.0
|
||||||
|
@ -48,6 +52,10 @@ shards:
|
||||||
git: https://github.com/juanedi/micrate.git
|
git: https://github.com/juanedi/micrate.git
|
||||||
version: 0.12.0
|
version: 0.12.0
|
||||||
|
|
||||||
|
mosquito:
|
||||||
|
git: https://github.com/mosquito-cr/mosquito.git
|
||||||
|
version: 0.11.1
|
||||||
|
|
||||||
openssl_ext:
|
openssl_ext:
|
||||||
git: https://github.com/spider-gazelle/openssl_ext.git
|
git: https://github.com/spider-gazelle/openssl_ext.git
|
||||||
version: 2.1.5
|
version: 2.1.5
|
||||||
|
@ -56,10 +64,26 @@ shards:
|
||||||
git: https://github.com/will/crystal-pg.git
|
git: https://github.com/will/crystal-pg.git
|
||||||
version: 0.24.0
|
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:
|
seg:
|
||||||
git: https://github.com/soveran/seg.git
|
git: https://github.com/soveran/seg.git
|
||||||
version: 0.1.0+git.commit.7f1cee94fb7ed7a2ba15f1388cbaede72a85eef9
|
version: 0.1.0+git.commit.7f1cee94fb7ed7a2ba15f1388cbaede72a85eef9
|
||||||
|
|
||||||
|
senf:
|
||||||
|
git: https://git.dergrimm.net/dergrimm/senf.git
|
||||||
|
version: 0.1.0
|
||||||
|
|
||||||
toro:
|
toro:
|
||||||
git: https://github.com/soveran/toro.git
|
git: https://github.com/soveran/toro.git
|
||||||
version: 0.4.3
|
version: 0.4.3
|
|
@ -1,12 +1,14 @@
|
||||||
name: api
|
name: backend
|
||||||
version: 0.1.0
|
version: 0.1.0
|
||||||
|
|
||||||
authors:
|
authors:
|
||||||
- Dominic Grimm <dominic.grimm@gmail.com>
|
- Dominic Grimm <dominic.grimm@gmail.com>
|
||||||
|
|
||||||
|
license: MIT
|
||||||
|
|
||||||
targets:
|
targets:
|
||||||
api:
|
backend:
|
||||||
main: src/api.cr
|
main: src/backend.cr
|
||||||
micrate:
|
micrate:
|
||||||
main: src/micrate.cr
|
main: src/micrate.cr
|
||||||
|
|
||||||
|
@ -35,3 +37,9 @@ dependencies:
|
||||||
github: Papierkorb/fancyline
|
github: Papierkorb/fancyline
|
||||||
micrate:
|
micrate:
|
||||||
github: juanedi/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 "granite/adapter/pg"
|
||||||
|
|
||||||
require "./db/*"
|
require "./db/*"
|
||||||
|
|
||||||
module API
|
module Backend
|
||||||
module Db
|
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
|
||||||
end
|
end
|
|
@ -1,6 +1,4 @@
|
||||||
require "granite"
|
module Backend
|
||||||
|
|
||||||
module API
|
|
||||||
module Db
|
module Db
|
||||||
class Admin < Granite::Base
|
class Admin < Granite::Base
|
||||||
table admins
|
table admins
|
|
@ -1,6 +1,4 @@
|
||||||
require "granite"
|
module Backend
|
||||||
|
|
||||||
module API
|
|
||||||
module Db
|
module Db
|
||||||
class Student < Granite::Base
|
class Student < Granite::Base
|
||||||
table students
|
table students
|
|
@ -1,6 +1,4 @@
|
||||||
require "granite"
|
module Backend
|
||||||
|
|
||||||
module API
|
|
||||||
module Db
|
module Db
|
||||||
class Teacher < Granite::Base
|
class Teacher < Granite::Base
|
||||||
table teachers
|
table teachers
|
|
@ -1,6 +1,4 @@
|
||||||
require "granite"
|
module Backend
|
||||||
|
|
||||||
module API
|
|
||||||
module Db
|
module Db
|
||||||
class TeacherVote < Granite::Base
|
class TeacherVote < Granite::Base
|
||||||
table teacher_votes
|
table teacher_votes
|
|
@ -1,7 +1,6 @@
|
||||||
require "CrystalEmail"
|
require "CrystalEmail"
|
||||||
require "granite"
|
|
||||||
|
|
||||||
module API
|
module Backend
|
||||||
module Db
|
module Db
|
||||||
class User < Granite::Base
|
class User < Granite::Base
|
||||||
table users
|
table users
|
|
@ -1,4 +1,4 @@
|
||||||
module API
|
module Backend
|
||||||
module Db
|
module Db
|
||||||
enum UserRole
|
enum UserRole
|
||||||
Admin
|
Admin
|
|
@ -1,6 +1,4 @@
|
||||||
require "granite"
|
module Backend
|
||||||
|
|
||||||
module API
|
|
||||||
module Db
|
module Db
|
||||||
class Vote < Granite::Base
|
class Vote < Granite::Base
|
||||||
table votes
|
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
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
docker-compose exec api micrate "$@"
|
docker-compose exec backend micrate "$@"
|
||||||
|
|
Loading…
Reference in a new issue