Documented Api
Some checks reported errors
continuous-integration/drone/push Build is running
continuous-integration/drone/pr Build was killed
continuous-integration/drone Build is passing

This commit is contained in:
Dominic Grimm 2022-02-09 14:35:35 +01:00
parent eed1042529
commit 046c33f45f
17 changed files with 134 additions and 32 deletions

View file

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

View file

@ -2,28 +2,36 @@ require "jwt"
module Backend
module Api
# Authorization and authentication utilities
module Auth
extend self
# Bearer token header
BEARER = "Bearer "
# Creates raw JWT token
#
# WARNING: Always use a wrapper for this method
private def create_jwt(data, expiration : Int) : String
JWT.encode({"data" => data.to_h, "exp" => expiration}, Backend.config.api.jwt_secret, JWT::Algorithm::HS256)
end
def create_user_jwt(user_id : Int, expiration : Int) : String
create_jwt({user_id: user_id}, expiration)
end
# Decodes JWT token
def decode_jwt(jwt : String) : JSON::Any
JWT.decode(jwt, Backend.config.api.jwt_secret, JWT::Algorithm::HS256)[0]
end
# :ditto:
def decode_jwt?(jwt : String) : JSON::Any?
decode_jwt(jwt)
rescue
nil
end
# Creates JWT token for user
def create_user_jwt(user_id : Int, expiration : Int) : String
create_jwt({user_id: user_id}, expiration)
end
end
end
end

View file

@ -4,10 +4,18 @@ require "granite"
module Backend
module Api
# GraphQL request context class
class Context < GraphQL::Context
# Authenticated user
getter user : Db::User?
# User is admin
getter admin : Bool?
# User's role
getter role : Schema::UserRole?
# User's external object
getter external : (Db::Teacher | Db::Student)?
def initialize(request : HTTP::Request, max_complexity : Int32? = nil)
@ -36,26 +44,31 @@ module Backend
end
end
# User is authenticated
def authenticated? : Bool
!!@user
end
# :ditto:
def authenticated! : Bool
raise "Not authenticated" unless authenticated?
true
end
# User is admin
def admin? : Bool
authenticated? && !!@admin
end
# :ditto:
def admin! : Bool
raise "Invalid permissions" unless admin?
true
end
# User's is one of *roles*
def role?(external = true, *roles : Schema::UserRole) : Bool
return false unless authenticated?
@ -75,51 +88,32 @@ module Backend
false
end
# :ditto:
def role!(external = true, *roles : Schema::UserRole) : Bool
raise "Invalid permissions" unless role? external, *roles
true
end
# User is teacher
def teacher?(external = false) : Bool
role? external, Schema::UserRole::Teacher
end
# :ditto:
def teacher! : Bool
role! external, Schema::UserRole::Teacher
end
# User is student
def student?(external = false) : Bool
role? external, Schema::UserRole::Student
end
# :ditto:
def student! : Bool
role! external, Schema::UserRole::Student
end
# private macro role_check(*roles)
# {% for role in roles %}
# {% name = role.names.last.underscore %}
# def {{ name }}?(external = true) : Bool
# role? external, {{ role }}
# end
# def {{ name }}!(external = true) : Bool
# role! external, {{ role }}
# end
# {% end %}
# end
# role_check Schema::UserRole::Teacher, Schema::UserRole::Student
# def self.db_eq_role?(external : Granite::Base, role : Schema::UserRole) : Bool
# role == case external
# when Db::Teacher
# Schema::UserRole::Teacher
# when Db::Student
# Schema::UserRole::Student
# end
# end
end
end
end

View file

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

View file

@ -5,7 +5,9 @@ require "./schema/*"
module Backend
module Api
# GraphQL schema definitions
module Schema
# Compiled GraphQL schema
SCHEMA = GraphQL::Schema.new(Query.new, Mutation.new)
end
end

View file

@ -1,8 +1,11 @@
module Backend
module Api
module Schema
# Schema helper macros
module Helpers
# Object helpers
module ObjectMacros
# Defines field property and GraphQL specific getter
macro field(type)
property {{ type.var }} {% if type.value %} = {{ type.value }}{% end %}
@ -13,7 +16,9 @@ module Backend
end
end
# DB model leverage helpers
module ObjectDbInit
# Defines a DB model specific initializer
macro db_init(type)
def initialize(obj : {{ type }})
initialize(obj.id.not_nil!)
@ -21,12 +26,16 @@ module Backend
end
end
# DB model finder helpers
module ObjectFinders
# Defines finder
macro finders(type)
# Finds object by ID
def find : {{ type }}?
{{ type }}.find(@id)
end
# :ditto:
def find! : {{ type }}
obj = find
raise "#{{{ type }}} not found" unless obj
@ -36,8 +45,12 @@ module Backend
end
end
# DB model field helpers
module DbObject
# Defines DB model field helper functions
macro db_object(type)
{% space_name = type.names.last.underscore.gsub(/_/, " ").capitalize %}
include ::Backend::Api::Schema::Helpers::ObjectDbInit
include ::Backend::Api::Schema::Helpers::ObjectFinders
@ -54,6 +67,7 @@ module Backend
end
@[GraphQL::Field]
# {{ space_name }}'s ID
def id : Int32
@id
end

View file

@ -6,6 +6,7 @@ module Backend
@[GraphQL::Object]
class Mutation < GraphQL::BaseMutation
@[GraphQL::Field]
# Logs in as *username* with credential *password*
def login(username : String, password : String) : LoginPayload
raise "Auth failed" if username.empty? || password.empty?
@ -22,6 +23,7 @@ module Backend
end
@[GraphQL::Field]
# Creates user
def create_user(context : Context, input : UserCreateInput, check_ldap : Bool = true) : User
context.admin!
@ -36,6 +38,7 @@ module Backend
end
@[GraphQL::Field]
# Deletes user by ID
def delete_user(context : Context, id : Int32) : Int32
context.admin!
@ -46,6 +49,7 @@ module Backend
end
@[GraphQL::Field]
# Sends all unregistered teachers a registration email
def send_teachers_registration_email(context : Context) : Bool
context.admin!
@ -55,6 +59,7 @@ module Backend
end
@[GraphQL::Field]
# Creates teacher
def create_teacher(context : Context, input : TeacherCreateInput) : Teacher
context.admin!
@ -63,6 +68,7 @@ module Backend
end
@[GraphQL::Field]
# Deletes teacher by ID
def delete_teacher(context : Context, id : Int32) : Int32
context.admin!
@ -73,6 +79,7 @@ module Backend
end
@[GraphQL::Field]
# Self register as teacher
def register_teacher(context : Context, input : TeacherInput) : Teacher
context.teacher? external: false
@ -82,6 +89,7 @@ module Backend
end
@[GraphQL::Field]
# Creates student
def create_student(context : Context, input : StudentCreateInput) : Student
context.admin!
@ -93,6 +101,7 @@ module Backend
end
@[GraphQL::Field]
# Deletes student by ID
def delete_student(context : Context, id : Int32) : Int32
context.admin!
@ -103,6 +112,7 @@ module Backend
end
@[GraphQL::Field]
# Creates vote for authenticated user's student
def create_vote(context : Context, input : VoteCreateInput) : Vote
context.student!

View file

@ -4,11 +4,13 @@ module Backend
@[GraphQL::Object]
class Query < GraphQL::BaseQuery
@[GraphQL::Field]
# Retuns true
def ok : Bool
true
end
@[GraphQL::Field]
# Current authenticated user
def me(context : Context) : User
context.authenticated!
@ -16,6 +18,7 @@ module Backend
end
@[GraphQL::Field]
# User by ID
def user(context : Context, id : Int32) : User
context.admin!
@ -23,6 +26,7 @@ module Backend
end
@[GraphQL::Field]
# All users
def users(context : Context) : Array(User)
context.admin!
@ -30,6 +34,7 @@ module Backend
end
@[GraphQL::Field]
# All admins
def admins(context : Context) : Array(User)
context.admin!
@ -37,16 +42,19 @@ module Backend
end
@[GraphQL::Field]
# Teacher by ID
def teacher(id : Int32) : Teacher
Teacher.new(Db::Teacher.find!(id))
end
@[GraphQL::Field]
# All teachers
def teachers : Array(Teacher)
Db::Teacher.all.map { |teacher| Teacher.new(teacher) }
end
@[GraphQL::Field]
# Student by ID
def student(context : Context, id : Int32) : Student
context.admin!
@ -54,6 +62,7 @@ module Backend
end
@[GraphQL::Field]
# All students
def students(context : Context) : Array(Student)
context.admin!
@ -61,6 +70,7 @@ module Backend
end
@[GraphQL::Field]
# Vote by ID
def vote(context : Context, id : Int32) : Vote
context.admin!
@ -68,6 +78,7 @@ module Backend
end
@[GraphQL::Field]
# All votes
def votes(context : Context) : Array(Vote)
context.admin!

View file

@ -2,31 +2,35 @@ module Backend
module Api
module Schema
@[GraphQL::Object]
# Student model
class Student < GraphQL::BaseObject
include Helpers::DbObject
db_object Db::Student
@[GraphQL::Field]
# Student's user
def user : User
User.new(find!.user)
end
@[GraphQL::Field]
# Student at SKIF
def skif : Bool
find!.skif
end
@[GraphQL::Field]
# Student's vote
def vote : Vote?
vote = find!.vote
Vote.new(vote) if vote
find!.vote.try { |v| Vote.new(v) }
end
end
@[GraphQL::InputObject]
# Student base input
class StudentInput < GraphQL::BaseInputObject
# Student at SKIF
getter skif
@[GraphQL::Field]
@ -35,7 +39,9 @@ module Backend
end
@[GraphQL::InputObject]
# Student creation input
class StudentCreateInput < StudentInput
# Student's user ID
getter user_id
@[GraphQL::Field]

View file

@ -2,30 +2,38 @@ module Backend
module Api
module Schema
@[GraphQL::Object]
# Teacher model
class Teacher < GraphQL::BaseObject
include Helpers::DbObject
db_object Db::Teacher
@[GraphQL::Field]
# Teacher's user
def user : User
User.new(find!.user)
end
@[GraphQL::Field]
# Teacher's max students
def max_students : Int32
find!.max_students
end
@[GraphQL::Field]
# Teacher is at SKIF
def skif : Bool
find!.skif
end
end
@[GraphQL::InputObject]
# Base teacher input
class TeacherInput < GraphQL::BaseInputObject
# Teacher's max students
getter max_students
# Teacher at SKIF
getter skif
@[GraphQL::Field]
@ -34,7 +42,9 @@ module Backend
end
@[GraphQL::InputObject]
# Teacher creation input
class TeacherCreateInput < TeacherInput
# Teacher's user ID
getter user_id
@[GraphQL::Field]

View file

@ -2,26 +2,35 @@ module Backend
module Api
module Schema
@[GraphQL::Object]
# Teacher vote model
class TeacherVote < GraphQL::BaseObject
include Helpers::DbObject
db_object Db::TeacherVote
@[GraphQL::Field]
# Voted teacher
def teacher : Teacher
Teacher.new(find!.teacher.not_nil!)
end
@[GraphQL::Field]
# Teacher vote's priority
def priority : Int32
find!.priority
end
end
@[GraphQL::InputObject]
# Teacher vote creation input
class TeacherVoteCreateInput < GraphQL::BaseInputObject
# Teacher vote's vote ID
getter vote_id
# Teacher vote's teacher ID
getter teacher_id
# Teacher vote's priority
getter priority
@[GraphQL::Field]

View file

@ -2,42 +2,50 @@ module Backend
module Api
module Schema
@[GraphQL::Object]
# User model
class User < GraphQL::BaseObject
include Helpers::DbObject
db_object Db::User
@[GraphQL::Field]
# User's first name
def firstname : String
find!.firstname
end
@[GraphQL::Field]
# User's last name
def lastname : String
find!.lastname
end
@[GraphQL::Field]
# User's full name
def name : String
find!.name
end
@[GraphQL::Field]
# User's LDAP username
def username : String
find!.username
end
@[GraphQL::Field]
# User's email
def email : String
find!.email
end
@[GraphQL::Field]
# User is admin
def admin : Bool
find!.admin
end
@[GraphQL::Field]
# User's role
def role : UserRole
role = Db::UserRole.parse(find!.role)
case role
@ -51,6 +59,7 @@ module Backend
end
@[GraphQL::Field]
# User's external ID
def external_id : Int32?
case Db::UserRole.parse(find!.role)
when .teacher?
@ -63,6 +72,7 @@ module Backend
end
@[GraphQL::Field]
# User's external teacher object
def teacher : Teacher?
teacher = find!.teacher
if teacher
@ -71,6 +81,7 @@ module Backend
end
@[GraphQL::Field]
# User's external student object
def student : Student?
student = find!.student
if student
@ -80,6 +91,7 @@ module Backend
end
@[GraphQL::InputObject]
# User creation input
class UserCreateInput < GraphQL::BaseInputObject
getter username
getter role
@ -93,8 +105,12 @@ module Backend
end
@[GraphQL::Object]
# Login payload returned after successful login
class LoginPayload < GraphQL::BaseObject
# Logged in user
property user
# JWT token
property token
def initialize(
@ -104,16 +120,19 @@ module Backend
end
@[GraphQL::Field]
# Logged in user
def user : User
@user
end
@[GraphQL::Field]
# Raw bearer token
def token : String
@token
end
@[GraphQL::Field]
# Ready to use bearer token
def bearer : String
Auth::BEARER + @token
end

View file

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

View file

@ -2,24 +2,29 @@ module Backend
module Api
module Schema
@[GraphQL::Object]
# Vote model
class Vote < GraphQL::BaseObject
include Helpers::DbObject
db_object Db::Vote
@[GraphQL::Field]
# Student who voted
def student : Student
Student.new(find!.student)
end
@[GraphQL::Field]
# Teacher votes for student
def teacher_votes : Array(TeacherVote)
find!.teacher_votes.map { |tv| TeacherVote.new(tv) }
end
end
@[GraphQL::InputObject]
# Vote creation input
class VoteCreateInput < GraphQL::BaseInputObject
# Teacher IDs student votes for
getter teacher_ids
@[GraphQL::Field]

View file

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

View file

@ -4,11 +4,16 @@ require "json"
module Backend
module Api
# Api webserver
class WebServer
include Router
# GraphQL playground HTML code
#
# NOTE: Is minified in production
GRAPHQL_PLAYGROUND = {{ flag?(:development) ? read_file("#{__DIR__}/playground.html") : run("./macros/minify_html.cr", read_file("#{__DIR__}/playground.html")).stringify }}
# GraphQL request data serializer
struct GraphQLQueryData
include JSON::Serializable
@ -17,6 +22,7 @@ module Backend
property operation_name : String?
end
# "Draws" (creates) routes
def draw_routes : Nil
# enable graphql playground when in development mode or explicitly enabled
if Backend.config.api.graphql_playground_fully_enabled?
@ -47,6 +53,7 @@ module Backend
end
end
# Runs the webserver with according middleware
def run : Nil
draw_routes

View file

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