Update ci
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
This commit is contained in:
parent
55afb91ece
commit
100f7c8ad6
|
@ -39,31 +39,35 @@ RUN shards install --production
|
||||||
FROM crystallang/crystal:1.6-alpine as builder
|
FROM crystallang/crystal:1.6-alpine as builder
|
||||||
WORKDIR /usr/src/mentorenwahl
|
WORKDIR /usr/src/mentorenwahl
|
||||||
RUN apk add --no-cache pcre2-dev
|
RUN apk add --no-cache pcre2-dev
|
||||||
|
RUN mkdir deps
|
||||||
COPY --from=deps /usr/src/mentorenwahl/shard.yml /usr/src/mentorenwahl/shard.lock ./
|
COPY --from=deps /usr/src/mentorenwahl/shard.yml /usr/src/mentorenwahl/shard.lock ./
|
||||||
COPY --from=deps /usr/src/mentorenwahl/lib ./lib
|
COPY --from=deps /usr/src/mentorenwahl/lib ./lib
|
||||||
COPY ./LICENSE .
|
|
||||||
COPY ./Makefile .
|
|
||||||
COPY ./db ./db
|
|
||||||
COPY ./src ./src
|
|
||||||
ARG BUILD_ENV
|
ARG BUILD_ENV
|
||||||
|
COPY ./Makefile .
|
||||||
|
COPY ./LICENSE .
|
||||||
|
COPY ./db ./db
|
||||||
COPY --from=public /usr/src/public/dist ./public
|
COPY --from=public /usr/src/public/dist ./public
|
||||||
|
COPY ./src ./src
|
||||||
RUN if [ "${BUILD_ENV}" = "development" ]; then \
|
RUN if [ "${BUILD_ENV}" = "development" ]; then \
|
||||||
make dev; \
|
make dev; \
|
||||||
else \
|
else \
|
||||||
make; \
|
make; \
|
||||||
fi
|
fi
|
||||||
RUN mkdir deps
|
|
||||||
RUN if [ "${BUILD_ENV}" = "development" ]; then \
|
RUN if [ "${BUILD_ENV}" = "development" ]; then \
|
||||||
ldd bin/backend | tr -s '[:blank:]' '\n' | grep '^/' | \
|
ldd bin/backend | tr -s '[:blank:]' '\n' | grep '^/' | \
|
||||||
xargs -I % sh -c 'mkdir -p $(dirname deps%); cp % deps%;'; \
|
xargs -I % sh -c 'mkdir -p $(dirname deps%); cp % deps%;'; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
FROM scratch as runner
|
FROM scratch as runner
|
||||||
|
LABEL maintainer="Dominic Grimm <dominic@dergrimm.net>" \
|
||||||
|
org.opencontainers.image.description="Backend of Mentorenwahl" \
|
||||||
|
org.opencontainers.image.licenses="GNU GPLv3" \
|
||||||
|
org.opencontainers.image.source="https://git.dergrimm.net/mentorenwahl/mentorenwahl" \
|
||||||
|
org.opencontainers.image.url="https://git.dergrimm.net/mentorenwahl/mentorenwahl"
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
COPY --from=micrate-builder /usr/src/micrate/bin/micrate ./bin/micrate
|
COPY --from=micrate-builder /usr/src/micrate/bin/micrate ./bin/micrate
|
||||||
|
COPY --from=builder /usr/src/mentorenwahl/db ./db
|
||||||
COPY --from=builder /usr/src/mentorenwahl/deps /
|
COPY --from=builder /usr/src/mentorenwahl/deps /
|
||||||
COPY --from=builder /usr/src/mentorenwahl/bin/backend ./bin/backend
|
COPY --from=builder /usr/src/mentorenwahl/bin/backend ./bin/backend
|
||||||
COPY --from=builder /usr/src/mentorenwahl/db ./db
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
ENTRYPOINT [ "./bin/backend" ]
|
CMD [ "./bin/backend", "run" ]
|
||||||
CMD [ "run" ]
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ CREATE TABLE tokens(
|
||||||
id uuid PRIMARY KEY,
|
id uuid PRIMARY KEY,
|
||||||
iat timestamp NOT NULL,
|
iat timestamp NOT NULL,
|
||||||
exp timestamp NOT NULL,
|
exp timestamp NOT NULL,
|
||||||
|
active boolean NOT NULL,
|
||||||
user_id int NOT NULL REFERENCES users(id)
|
user_id int NOT NULL REFERENCES users(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ require "graphql"
|
||||||
require "http/headers"
|
require "http/headers"
|
||||||
require "jwt/errors"
|
require "jwt/errors"
|
||||||
require "json"
|
require "json"
|
||||||
|
require "uuid"
|
||||||
|
|
||||||
module Backend
|
module Backend
|
||||||
module Api
|
module Api
|
||||||
|
@ -41,13 +42,17 @@ module Backend
|
||||||
# User's external object
|
# User's external object
|
||||||
getter external
|
getter external
|
||||||
|
|
||||||
|
# JTI of request token
|
||||||
|
getter jti
|
||||||
|
|
||||||
def initialize(
|
def initialize(
|
||||||
@development : Bool,
|
@development : Bool,
|
||||||
@status : Status,
|
@status : Status,
|
||||||
@user : Db::User?,
|
@user : Db::User?,
|
||||||
@admin : Bool?,
|
@admin : Bool?,
|
||||||
@role : Schema::UserRole?,
|
@role : Schema::UserRole?,
|
||||||
@external : (Db::Teacher | Db::Student)?
|
@external : (Db::Teacher | Db::Student)?,
|
||||||
|
@jti : UUID?
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -63,15 +68,20 @@ module Backend
|
||||||
@status = Status::JWTError
|
@status = Status::JWTError
|
||||||
else
|
else
|
||||||
if @user = Db::User.find(payload.context.user)
|
if @user = Db::User.find(payload.context.user)
|
||||||
@admin = user.not_nil!.admin
|
@jti = payload.jti
|
||||||
@role = user.not_nil!.role.to_api
|
if Db::Token.query.where { (id == payload.jti) & active }.first.nil?
|
||||||
@external =
|
@status = Status::SessionExpired
|
||||||
case @role.not_nil!
|
else
|
||||||
when .teacher?
|
@admin = user.not_nil!.admin
|
||||||
@user.not_nil!.teacher
|
@role = user.not_nil!.role.to_api
|
||||||
when .student?
|
@external =
|
||||||
@user.not_nil!.student
|
case @role.not_nil!
|
||||||
end
|
when .teacher?
|
||||||
|
@user.not_nil!.teacher
|
||||||
|
when .student?
|
||||||
|
@user.not_nil!.student
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,7 +20,7 @@ module Backend
|
||||||
# Schema helper macros
|
# Schema helper macros
|
||||||
module Helpers
|
module Helpers
|
||||||
# Defines DB model field helper functions
|
# Defines DB model field helper functions
|
||||||
macro db_object(type)
|
macro db_object(type, id_type)
|
||||||
def initialize(@model : {{ type }})
|
def initialize(@model : {{ type }})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ module Backend
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
# {{ space_name }}'s ID
|
# {{ space_name }}'s ID
|
||||||
def id : Int32
|
def id : {{ id_type }}
|
||||||
@model.id
|
@model.id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -36,9 +36,9 @@ module Backend
|
||||||
id: UUID.random(Random::Secure),
|
id: UUID.random(Random::Secure),
|
||||||
iat: Time.utc,
|
iat: Time.utc,
|
||||||
exp: Time.utc + Backend.config.api.jwt_expiration.minutes,
|
exp: Time.utc + Backend.config.api.jwt_expiration.minutes,
|
||||||
|
active: true,
|
||||||
user_id: user.id
|
user_id: user.id
|
||||||
)
|
)
|
||||||
pp! token, typeof(token.id)
|
|
||||||
|
|
||||||
LoginPayload.new(
|
LoginPayload.new(
|
||||||
user: User.new(user),
|
user: User.new(user),
|
||||||
|
@ -51,6 +51,29 @@ module Backend
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@[GraphQL::Field]
|
||||||
|
# Logs out of account by revoking token
|
||||||
|
def logout(context : Context) : Scalars::UUID?
|
||||||
|
context.authenticated!
|
||||||
|
|
||||||
|
jti = context.jti.not_nil!
|
||||||
|
|
||||||
|
token = Db::Token.find!(jti)
|
||||||
|
token.active = false
|
||||||
|
token.save!
|
||||||
|
|
||||||
|
Scalars::UUID.new(jti)
|
||||||
|
end
|
||||||
|
|
||||||
|
@[GraphQL::Field]
|
||||||
|
def revoke_token(context : Context, token : Scalars::UUID) : Scalars::UUID
|
||||||
|
context.authenticated!
|
||||||
|
|
||||||
|
Db::Token.query.find!(token.value).delete
|
||||||
|
|
||||||
|
Scalars::UUID.new(token.value)
|
||||||
|
end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
# Creates user
|
# Creates user
|
||||||
def create_user(context : Context, input : UserCreateInput, check_ldap : Bool = true) : User?
|
def create_user(context : Context, input : UserCreateInput, check_ldap : Bool = true) : User?
|
||||||
|
|
|
@ -39,6 +39,16 @@ module Backend
|
||||||
User.new(context.user.not_nil!)
|
User.new(context.user.not_nil!)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@[GraphQL::Field]
|
||||||
|
# Active tokens of the current user
|
||||||
|
def tokens(context : Context) : Array(Token)?
|
||||||
|
context.authenticated!
|
||||||
|
|
||||||
|
now = Time.utc
|
||||||
|
|
||||||
|
Db::Token.query.where { (user_id == context.user.not_nil!.id) & (exp > now) & active }.map { |t| Token.new(t) }
|
||||||
|
end
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
# User by ID
|
# User by ID
|
||||||
def user(context : Context, id : Int32) : User?
|
def user(context : Context, id : Int32) : User?
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
require "./scalars/*"
|
|
@ -0,0 +1,17 @@
|
||||||
|
module Backend::Api::Schema::Scalars
|
||||||
|
@[GraphQL::Scalar]
|
||||||
|
class Time < GraphQL::BaseScalar
|
||||||
|
property value
|
||||||
|
|
||||||
|
def initialize(@value : ::Time)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.from_json(string_or_io)
|
||||||
|
self.new(::Time.parse_iso8601(string_or_io))
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_json(builder : JSON::Builder)
|
||||||
|
builder.scalar(@value.to_rfc3339)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,19 @@
|
||||||
|
require "uuid"
|
||||||
|
|
||||||
|
module Backend::Api::Schema::Scalars
|
||||||
|
@[GraphQL::Scalar]
|
||||||
|
class UUID < GraphQL::BaseScalar
|
||||||
|
property value
|
||||||
|
|
||||||
|
def initialize(@value : ::UUID)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.from_json(string_or_io)
|
||||||
|
self.new(::UUID.from_json(string_or_io))
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_json(builder : JSON::Builder)
|
||||||
|
builder.scalar(@value.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -22,7 +22,7 @@ module Backend
|
||||||
class Student < GraphQL::BaseObject
|
class Student < GraphQL::BaseObject
|
||||||
include Helpers
|
include Helpers
|
||||||
|
|
||||||
db_object Db::Student
|
db_object Db::Student, Int32
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
# Student's user
|
# Student's user
|
||||||
|
|
|
@ -22,7 +22,7 @@ module Backend
|
||||||
class Teacher < GraphQL::BaseObject
|
class Teacher < GraphQL::BaseObject
|
||||||
include Helpers
|
include Helpers
|
||||||
|
|
||||||
db_object Db::Teacher
|
db_object Db::Teacher, Int32
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
# Teacher's user
|
# Teacher's user
|
||||||
|
|
|
@ -22,7 +22,7 @@ module Backend
|
||||||
class TeacherVote < GraphQL::BaseObject
|
class TeacherVote < GraphQL::BaseObject
|
||||||
include Helpers
|
include Helpers
|
||||||
|
|
||||||
db_object Db::TeacherVote
|
db_object Db::TeacherVote, Int32
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
# Voted teacher
|
# Voted teacher
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
module Backend::Api::Schema
|
||||||
|
@[GraphQL::Object]
|
||||||
|
# Token model
|
||||||
|
class Token < GraphQL::BaseObject
|
||||||
|
include Helpers
|
||||||
|
|
||||||
|
def initialize(@model : Db::Token)
|
||||||
|
end
|
||||||
|
|
||||||
|
@[GraphQL::Field]
|
||||||
|
# Token ID / JTI
|
||||||
|
def id : Scalars::UUID
|
||||||
|
Scalars::UUID.new(@model.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
@[GraphQL::Field]
|
||||||
|
# Time the token was issued at
|
||||||
|
def iat : Scalars::Time
|
||||||
|
Scalars::Time.new(@model.iat)
|
||||||
|
end
|
||||||
|
|
||||||
|
@[GraphQL::Field]
|
||||||
|
# Time for the token to expire
|
||||||
|
def exp : Scalars::Time
|
||||||
|
Scalars::Time.new(@model.exp)
|
||||||
|
end
|
||||||
|
|
||||||
|
@[GraphQL::Field]
|
||||||
|
# Token is expired
|
||||||
|
def expired : Bool
|
||||||
|
@model.exp > Time.utc
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -44,7 +44,7 @@ module Backend
|
||||||
class User < GraphQL::BaseObject
|
class User < GraphQL::BaseObject
|
||||||
include Helpers
|
include Helpers
|
||||||
|
|
||||||
db_object Db::User
|
db_object Db::User, Int32
|
||||||
|
|
||||||
@ldap : Ldap::User?
|
@ldap : Ldap::User?
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ module Backend
|
||||||
class Vote < GraphQL::BaseObject
|
class Vote < GraphQL::BaseObject
|
||||||
include Helpers
|
include Helpers
|
||||||
|
|
||||||
db_object Db::Vote
|
db_object Db::Vote, Int32
|
||||||
|
|
||||||
@[GraphQL::Field]
|
@[GraphQL::Field]
|
||||||
# Student who voted
|
# Student who voted
|
||||||
|
|
|
@ -14,16 +14,14 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
module Backend
|
module Backend::Db
|
||||||
module Db
|
class Assignment
|
||||||
class Assignment
|
include Clear::Model
|
||||||
include Clear::Model
|
self.table = :assignments
|
||||||
self.table = :assignments
|
|
||||||
|
|
||||||
primary_key type: :serial
|
primary_key type: :serial
|
||||||
|
|
||||||
belongs_to student : Student
|
belongs_to student : Student
|
||||||
belongs_to teacher : Teacher
|
belongs_to teacher : Teacher
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
module Backend
|
module Backend::Db
|
||||||
module Db
|
class MicrateDbVersion
|
||||||
class MicrateDbVersion
|
include Clear::Model
|
||||||
include Clear::Model
|
self.table = :micrate_db_version
|
||||||
self.table = :micrate_db_version
|
|
||||||
|
|
||||||
primary_key type: :serial
|
primary_key type: :serial
|
||||||
|
|
||||||
column version_id : Int64
|
column version_id : Int64
|
||||||
column is_applied : Bool
|
column is_applied : Bool
|
||||||
column tstamp : Time?
|
column tstamp : Time?
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
module Backend
|
module Backend::Db
|
||||||
module Db
|
class Student
|
||||||
class Student
|
include Clear::Model
|
||||||
include Clear::Model
|
self.table = :students
|
||||||
self.table = :students
|
|
||||||
|
|
||||||
primary_key type: :serial
|
primary_key type: :serial
|
||||||
|
|
||||||
belongs_to user : User
|
belongs_to user : User
|
||||||
|
|
||||||
has_one vote : Vote?, foreign_key: :student_id
|
has_one vote : Vote?, foreign_key: :student_id
|
||||||
has_one assignment : Assignment?, foreign_key: :student_id
|
has_one assignment : Assignment?, foreign_key: :student_id
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,17 +1,15 @@
|
||||||
module Backend
|
module Backend::Db
|
||||||
module Db
|
class Teacher
|
||||||
class Teacher
|
include Clear::Model
|
||||||
include Clear::Model
|
self.table = :teachers
|
||||||
self.table = :teachers
|
|
||||||
|
|
||||||
primary_key type: :serial
|
primary_key type: :serial
|
||||||
|
|
||||||
belongs_to user : User
|
belongs_to user : User
|
||||||
|
|
||||||
column max_students : Int32
|
column max_students : Int32
|
||||||
|
|
||||||
has_many teacher_votes : TeacherVote, foreign_key: :teacher_id
|
has_many teacher_votes : TeacherVote, foreign_key: :teacher_id
|
||||||
has_many assignments : Assignment, foreign_key: :teacher_id
|
has_many assignments : Assignment, foreign_key: :teacher_id
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
module Backend
|
module Backend::Db
|
||||||
module Db
|
class TeacherVote
|
||||||
class TeacherVote
|
include Clear::Model
|
||||||
include Clear::Model
|
self.table = :teacher_votes
|
||||||
self.table = :teacher_votes
|
|
||||||
|
|
||||||
primary_key type: :serial
|
primary_key type: :serial
|
||||||
|
|
||||||
belongs_to vote : Vote
|
belongs_to vote : Vote
|
||||||
belongs_to teacher : Teacher
|
belongs_to teacher : Teacher
|
||||||
|
|
||||||
column priority : Int32
|
column priority : Int32
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,6 +7,7 @@ module Backend::Db
|
||||||
|
|
||||||
column iat : Time
|
column iat : Time
|
||||||
column exp : Time
|
column exp : Time
|
||||||
|
column active : Bool
|
||||||
|
|
||||||
belongs_to user : User
|
belongs_to user : User
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,40 +1,38 @@
|
||||||
module Backend
|
module Backend::Db
|
||||||
module Db
|
Clear.enum UserRole, :teacher, :student
|
||||||
Clear.enum UserRole, :teacher, :student
|
|
||||||
|
|
||||||
struct UserRole
|
struct UserRole
|
||||||
# API representation of the enum
|
# API representation of the enum
|
||||||
def to_api : Api::Schema::UserRole
|
def to_api : Api::Schema::UserRole
|
||||||
case self
|
case self
|
||||||
when Student
|
when Student
|
||||||
Api::Schema::UserRole::Student
|
Api::Schema::UserRole::Student
|
||||||
when Teacher
|
when Teacher
|
||||||
Api::Schema::UserRole::Teacher
|
Api::Schema::UserRole::Teacher
|
||||||
else
|
else
|
||||||
raise "Invalid enum value for UserRole"
|
raise "Invalid enum value for UserRole"
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# DB representation of the enum
|
|
||||||
def self.from_api(role : Api::Schema::UserRole) : self
|
|
||||||
role.to_db
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class User
|
# DB representation of the enum
|
||||||
include Clear::Model
|
def self.from_api(role : Api::Schema::UserRole) : self
|
||||||
self.table = :users
|
role.to_db
|
||||||
|
|
||||||
primary_key type: :serial
|
|
||||||
|
|
||||||
column username : String
|
|
||||||
column role : UserRole
|
|
||||||
column admin : Bool = false
|
|
||||||
|
|
||||||
has_one student : Student?, foreign_key: :user_id
|
|
||||||
has_one teacher : Teacher?, foreign_key: :user_id
|
|
||||||
|
|
||||||
has_many tokens : Token, foreign_key: :user_id
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class User
|
||||||
|
include Clear::Model
|
||||||
|
self.table = :users
|
||||||
|
|
||||||
|
primary_key type: :serial
|
||||||
|
|
||||||
|
column username : String
|
||||||
|
column role : UserRole
|
||||||
|
column admin : Bool = false
|
||||||
|
|
||||||
|
has_one student : Student?, foreign_key: :user_id
|
||||||
|
has_one teacher : Teacher?, foreign_key: :user_id
|
||||||
|
|
||||||
|
has_many tokens : Token, foreign_key: :user_id
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
module Backend
|
module Backend::Db
|
||||||
module Db
|
class Vote
|
||||||
class Vote
|
include Clear::Model
|
||||||
include Clear::Model
|
self.table = :votes
|
||||||
self.table = :votes
|
|
||||||
|
|
||||||
primary_key type: :serial
|
primary_key type: :serial
|
||||||
|
|
||||||
belongs_to student : Student
|
belongs_to student : Student
|
||||||
|
|
||||||
has_many teacher_votes : TeacherVote, foreign_key: :vote_id
|
has_many teacher_votes : TeacherVote, foreign_key: :vote_id
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,28 +14,27 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
require "random/secure"
|
|
||||||
|
|
||||||
module Backend
|
module Backend
|
||||||
module Worker
|
module Worker
|
||||||
module Jobs
|
module Jobs
|
||||||
# Assigns students to teachers when all students voted
|
# Assigns students to teachers when all students voted
|
||||||
class AssignStudentsJob < Mosquito::QueuedJob
|
class AssignStudentsJob < Mosquito::QueuedJob
|
||||||
def rescheduleable?
|
# run_every 1.minute
|
||||||
|
|
||||||
|
def rescheduleable? : Bool
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
alias TeacherVote = {student: Int32, priority: Int32}
|
||||||
|
alias Assignment = {teacher: Int32, priority: Int32}
|
||||||
|
|
||||||
# :ditto:
|
# :ditto:
|
||||||
def perform : Nil
|
def perform : Nil
|
||||||
user_count = Db::User.query.count
|
|
||||||
teacher_count = Db::Teacher.query.count
|
teacher_count = Db::Teacher.query.count
|
||||||
student_count = Db::Student.query.count
|
student_count = Db::Student.query.count
|
||||||
vote_count = Db::Vote.query.count
|
vote_count = Db::Vote.query.count
|
||||||
|
|
||||||
if user_count == 0
|
if teacher_count == 0
|
||||||
log "No users found, skipping assignment"
|
|
||||||
fail
|
|
||||||
elsif teacher_count == 0
|
|
||||||
log "No teachers found, skipping assignment"
|
log "No teachers found, skipping assignment"
|
||||||
fail
|
fail
|
||||||
elsif student_count == 0
|
elsif student_count == 0
|
||||||
|
@ -47,67 +46,123 @@ module Backend
|
||||||
elsif vote_count < student_count
|
elsif vote_count < student_count
|
||||||
log "Not all students voted, skipping assignment"
|
log "Not all students voted, skipping assignment"
|
||||||
fail
|
fail
|
||||||
|
elsif Db::Assignment.query.count > 0
|
||||||
|
log "Assignment has already run, skipping another assignment"
|
||||||
|
fail
|
||||||
end
|
end
|
||||||
|
|
||||||
# possibilities = [] of Possibility
|
teachers = Db::Teacher.query
|
||||||
# teachers = Db::Teacher.query.to_a.select! { |t| t.assignments.count < t.max_students }
|
|
||||||
# empty_assignment = Hash.zip(teachers.map(&.id), [[] of StudentAssignment] * teachers.size)
|
|
||||||
# random_votes = Db::Student.query.to_a.select!(&.vote).map do |s|
|
|
||||||
# {
|
|
||||||
# student: s.id,
|
|
||||||
# teachers: s.vote.not_nil!.teacher_votes.to_a
|
|
||||||
# .sort_by!(&.priority)
|
|
||||||
# .reverse!
|
|
||||||
# .map(&.teacher.id),
|
|
||||||
# }
|
|
||||||
# end
|
|
||||||
|
|
||||||
# Backend.config.assignment_retry_count.times do
|
|
||||||
# pp! random_votes.shuffle!(Random::Secure)
|
|
||||||
# a = empty_assignment.clone
|
|
||||||
# random_votes.
|
|
||||||
|
|
||||||
# possibilities << {assignment: a, weighting: 0}
|
|
||||||
# end
|
|
||||||
|
|
||||||
# pp! possibilities
|
|
||||||
|
|
||||||
# teacher_ids = Db::Teacher.query
|
|
||||||
# .select("id")
|
|
||||||
# .where { raw("(SELECT COUNT(*) FROM assignments WHERE teacher_id = teachers.id)") < max_students }
|
|
||||||
# .map(&.id)
|
|
||||||
students = Db::Student.query
|
|
||||||
.where do
|
.where do
|
||||||
raw("NOT EXISTS (SELECT 1 FROM assignments WHERE student_id = students.id)") &
|
raw("EXISTS (SELECT 1 FROM teacher_votes WHERE teacher_id = teachers.id)") &
|
||||||
raw("EXISTS (SELECT 1 FROM votes WHERE student_id = students.id)")
|
(max_students > 0)
|
||||||
end
|
end
|
||||||
.with_vote(&.with_teacher_votes(&.order_by(:priority, :asc)))
|
.with_teacher_votes
|
||||||
# student_ids = students.map(&.id)
|
.to_a
|
||||||
|
vote_index = Hash.zip(teachers.map(&.id), [0] * teachers.size)
|
||||||
|
teacher_votes : Hash(Int32, Array(TeacherVote)) = Hash.zip(
|
||||||
|
teachers.map(&.id),
|
||||||
|
teachers.map do |t|
|
||||||
|
t.teacher_votes.map do |tv|
|
||||||
|
vote_index[t.id] += 1
|
||||||
|
|
||||||
# votes = Hash.zip(
|
{
|
||||||
# student_ids,
|
student: tv.vote.student.id,
|
||||||
# students.map do |s|
|
priority: tv.priority,
|
||||||
# s.vote.not_nil!.teacher_votes.map(&.teacher_id)
|
}
|
||||||
# end,
|
end
|
||||||
# )
|
end
|
||||||
# pp! votes, student_ids
|
)
|
||||||
|
teachers.sort_by! { |t| vote_index[t.id] }
|
||||||
|
|
||||||
random_votes = students.map do |s|
|
students = Db::Student.query
|
||||||
{
|
.with_vote(&.with_teacher_votes(&.order_by(priority: :asc)))
|
||||||
student: s.id,
|
.to_a
|
||||||
teachers: s.vote.not_nil!.teacher_votes.map(&.teacher_id),
|
student_ids = students.map(&.id)
|
||||||
}
|
votes = Hash.zip(
|
||||||
|
student_ids,
|
||||||
|
students.map do |s|
|
||||||
|
s.vote.not_nil!.teacher_votes
|
||||||
|
.to_a
|
||||||
|
.select { |tv| teacher_votes.has_key?(tv.teacher.id) }
|
||||||
|
.map do |tv|
|
||||||
|
{
|
||||||
|
teacher: tv.teacher.id,
|
||||||
|
teacher_max_students: tv.teacher.max_students,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
best_assignment = {
|
||||||
|
assignment: {} of Int32 => Assignment,
|
||||||
|
score: Float32::INFINITY,
|
||||||
|
}
|
||||||
|
|
||||||
|
Backend.config.assignment_possibility_count.times.each do
|
||||||
|
assignment = {} of Int32 => Assignment
|
||||||
|
assignment_count = Hash.zip(teachers.map(&.id), [0] * teachers.size)
|
||||||
|
# teachers.each do |t|
|
||||||
|
# queue = Deque.new(teacher_votes[t.id].shuffle)
|
||||||
|
|
||||||
|
# count = 1
|
||||||
|
# while count < t.max_students
|
||||||
|
# break unless x = queue.shift?
|
||||||
|
# tv = x.not_nil!
|
||||||
|
|
||||||
|
# if assignment[tv[:student]]?.nil? || assignment[tv[:student]][:priority] <= tv[:priority]
|
||||||
|
# assignment[tv[:student]] = {teacher: t.id, priority: tv[:priority]}
|
||||||
|
# count += 1
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
votes.to_a.shuffle.each do |s, tvs|
|
||||||
|
tvs.each_with_index do |tv, i|
|
||||||
|
if assignment[s]?.nil?
|
||||||
|
assignment_count[tv[:teacher]] += 1
|
||||||
|
assignment[s] = {teacher: tv[:teacher], priority: i}
|
||||||
|
elsif assignment_count[tv[:teacher]] < tv[:teacher_max_students]
|
||||||
|
assignment_count[assignment[s][:teacher]] -= 1
|
||||||
|
assignment_count[tv[:teacher]] += 1
|
||||||
|
assignment[s] = {teacher: tv[:teacher], priority: i}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
pp! assignment, assignment_count
|
||||||
|
|
||||||
|
score = 0_f32
|
||||||
|
# positivity = 0
|
||||||
|
# assignment.each do |s, a|
|
||||||
|
# ratio = (vote_sizes[s] - a[:priority]) / vote_sizes[s]
|
||||||
|
# score += 2 ** ratio
|
||||||
|
# positivity += ratio > 0.5 ? 1 : -1
|
||||||
|
# end
|
||||||
|
assignment.each do |s, a|
|
||||||
|
size = votes[s].size
|
||||||
|
p! a[:priority], (votes[s].size - a[:priority]) / size
|
||||||
|
# score += 1 if ((votes[s].size - a[:priority]) / size) >= 0.5
|
||||||
|
score += a[:priority]
|
||||||
|
end
|
||||||
|
|
||||||
|
# full_score = score ** positivity
|
||||||
|
if score < best_assignment[:score]
|
||||||
|
best_assignment = {
|
||||||
|
assignment: assignment,
|
||||||
|
score: score,
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
assignments = [] of {assignment: Hash(Int32, Array(Int32)), weighting: Int32}
|
pp! best_assignment
|
||||||
# empty_assignment = Hash.zip(teacher_ids, [[] of {assignment: Hash(Int32, Array(Int32)), weighting: Int32}] * teacher_ids.size)
|
|
||||||
Backend.config.assignment_possibility_count.times do
|
|
||||||
random_votes.shuffle!(Random::Secure)
|
|
||||||
|
|
||||||
# votes = random_votes.dup
|
str = String.build do |str|
|
||||||
# a = empty_assignment.clone
|
str << "===========================\n"
|
||||||
|
best_assignment[:assignment].each do |s, a|
|
||||||
|
str << "#{Db::Student.query.find!(s).user.username} : #{Db::Teacher.query.find!(a[:teacher]).user.username} (#{a[:priority]} / #{votes[s].size})\n"
|
||||||
|
end
|
||||||
|
str << "===========================\n"
|
||||||
end
|
end
|
||||||
pp! assignments
|
print str
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,9 +15,13 @@
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
events {
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
}
|
}
|
||||||
|
|
||||||
http {
|
http {
|
||||||
|
server_tokens off;
|
||||||
|
more_clear_headers Server;
|
||||||
|
|
||||||
server {
|
server {
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://frontend/;
|
proxy_pass http://frontend/;
|
||||||
|
|
|
@ -18,7 +18,7 @@ version: "3"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:alpine
|
image: byjg/nginx-extras
|
||||||
restart: always
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- ./config/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
- ./config/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
@ -58,7 +58,7 @@ services:
|
||||||
- redis:/data
|
- redis:/data
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
image: mentorenwahl/backend
|
image: git.dergrimm.net/mentorenwahl/backend:latest
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./backend
|
||||||
args:
|
args:
|
||||||
|
@ -96,7 +96,7 @@ services:
|
||||||
BACKEND_LDAP_CACHE_REFRESH_INTERVAL: ${BACKEND_LDAP_CACHE_REFRESH_INTERVAL}
|
BACKEND_LDAP_CACHE_REFRESH_INTERVAL: ${BACKEND_LDAP_CACHE_REFRESH_INTERVAL}
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: mentorenwahl/frontend
|
image: git.dergrimm.net/mentorenwahl/frontend:latest
|
||||||
build:
|
build:
|
||||||
context: ./frontend
|
context: ./frontend
|
||||||
restart: always
|
restart: always
|
||||||
|
|
|
@ -36,6 +36,11 @@ WORKDIR /usr/src/public
|
||||||
COPY --from=public /usr/src/public .
|
COPY --from=public /usr/src/public .
|
||||||
RUN find . -name "*.wasm" -type f | xargs -I % wasm-opt % -o % -O --intrinsic-lowering -Oz
|
RUN find . -name "*.wasm" -type f | xargs -I % wasm-opt % -o % -O --intrinsic-lowering -Oz
|
||||||
|
|
||||||
FROM nginx:alpine as runner
|
FROM byjg/nginx-extras as runner
|
||||||
|
LABEL maintainer="Dominic Grimm <dominic@dergrimm.net>" \
|
||||||
|
org.opencontainers.image.description="Frontend of Mentorenwahl" \
|
||||||
|
org.opencontainers.image.licenses="GNU GPLv3" \
|
||||||
|
org.opencontainers.image.source="https://git.dergrimm.net/mentorenwahl/mentorenwahl" \
|
||||||
|
org.opencontainers.image.url="https://git.dergrimm.net/mentorenwahl/mentorenwahl"
|
||||||
COPY ./nginx.conf /etc/nginx/nginx.conf
|
COPY ./nginx.conf /etc/nginx/nginx.conf
|
||||||
COPY --from=binaryen /usr/src/public /var/www/html
|
COPY --from=binaryen /usr/src/public /var/www/html
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
mutation Logout {
|
||||||
|
logout
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
mutation RevokeToken($token: UUID!) {
|
||||||
|
revokeToken(token: $token)
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
query Tokens {
|
||||||
|
tokens {
|
||||||
|
id
|
||||||
|
iat
|
||||||
|
exp
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ type Query {
|
||||||
teacherVote(id: Int!): TeacherVote
|
teacherVote(id: Int!): TeacherVote
|
||||||
teacherVotes: [TeacherVote!]
|
teacherVotes: [TeacherVote!]
|
||||||
teachers: [Teacher!]!
|
teachers: [Teacher!]!
|
||||||
|
tokens: [Token!]
|
||||||
user(id: Int!): User
|
user(id: Int!): User
|
||||||
userByUsername(username: String!): User
|
userByUsername(username: String!): User
|
||||||
users: [User!]
|
users: [User!]
|
||||||
|
@ -42,6 +43,17 @@ type TeacherVote {
|
||||||
vote: Vote!
|
vote: Vote!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scalar Time
|
||||||
|
|
||||||
|
type Token {
|
||||||
|
exp: Time!
|
||||||
|
expired: Boolean!
|
||||||
|
iat: Time!
|
||||||
|
id: UUID!
|
||||||
|
}
|
||||||
|
|
||||||
|
scalar UUID
|
||||||
|
|
||||||
type User {
|
type User {
|
||||||
admin: Boolean!
|
admin: Boolean!
|
||||||
email: String!
|
email: String!
|
||||||
|
@ -79,7 +91,9 @@ type Mutation {
|
||||||
createVote(input: VoteCreateInput!): Vote
|
createVote(input: VoteCreateInput!): Vote
|
||||||
deleteUser(id: Int!): Int
|
deleteUser(id: Int!): Int
|
||||||
login(password: String!, username: String!): LoginPayload
|
login(password: String!, username: String!): LoginPayload
|
||||||
|
logout: UUID
|
||||||
registerTeacher(input: TeacherInput!): Teacher
|
registerTeacher(input: TeacherInput!): Teacher
|
||||||
|
revokeToken(token: UUID!): UUID!
|
||||||
}
|
}
|
||||||
|
|
||||||
input TeacherInput {
|
input TeacherInput {
|
||||||
|
|
|
@ -1,29 +1,10 @@
|
||||||
user nginx;
|
|
||||||
worker_processes auto;
|
|
||||||
|
|
||||||
error_log /var/log/nginx/error.log notice;
|
|
||||||
pid /var/run/nginx.pid;
|
|
||||||
|
|
||||||
events {
|
events {
|
||||||
worker_connections 1024;
|
worker_connections 1024;
|
||||||
}
|
}
|
||||||
|
|
||||||
http {
|
http {
|
||||||
include /etc/nginx/mime.types;
|
server_tokens off;
|
||||||
default_type application/octet-stream;
|
more_clear_headers Server;
|
||||||
|
|
||||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
|
||||||
'$status $body_bytes_sent "$http_referer" '
|
|
||||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
|
||||||
|
|
||||||
access_log /var/log/nginx/access.log main;
|
|
||||||
|
|
||||||
sendfile on;
|
|
||||||
tcp_nopush on;
|
|
||||||
|
|
||||||
keepalive_timeout 65;
|
|
||||||
|
|
||||||
#gzip on;
|
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
|
|
|
@ -4,6 +4,7 @@ use graphql_client::GraphQLQuery;
|
||||||
#[graphql(
|
#[graphql(
|
||||||
schema_path = "graphql/schema.graphql",
|
schema_path = "graphql/schema.graphql",
|
||||||
query_path = "graphql/mutations/login.graphql",
|
query_path = "graphql/mutations/login.graphql",
|
||||||
response_derives = "Debug"
|
response_derives = "Debug",
|
||||||
|
skip_serializing_none
|
||||||
)]
|
)]
|
||||||
pub struct Login;
|
pub struct Login;
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
use graphql_client::GraphQLQuery;
|
||||||
|
|
||||||
|
type UUID = String;
|
||||||
|
|
||||||
|
#[derive(GraphQLQuery)]
|
||||||
|
#[graphql(
|
||||||
|
schema_path = "graphql/schema.graphql",
|
||||||
|
query_path = "graphql/mutations/logout.graphql",
|
||||||
|
response_derives = "Debug",
|
||||||
|
skip_serializing_none
|
||||||
|
)]
|
||||||
|
pub struct Logout;
|
|
@ -1,3 +1,5 @@
|
||||||
pub mod login;
|
pub mod login;
|
||||||
|
pub mod logout;
|
||||||
pub mod register_teacher;
|
pub mod register_teacher;
|
||||||
|
pub mod revoke_token;
|
||||||
pub mod vote;
|
pub mod vote;
|
||||||
|
|
|
@ -4,6 +4,7 @@ use graphql_client::GraphQLQuery;
|
||||||
#[graphql(
|
#[graphql(
|
||||||
schema_path = "graphql/schema.graphql",
|
schema_path = "graphql/schema.graphql",
|
||||||
query_path = "graphql/mutations/register_teacher.graphql",
|
query_path = "graphql/mutations/register_teacher.graphql",
|
||||||
response_derives = "Debug"
|
response_derives = "Debug",
|
||||||
|
skip_serializing_none
|
||||||
)]
|
)]
|
||||||
pub struct RegisterTeacher;
|
pub struct RegisterTeacher;
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
use graphql_client::GraphQLQuery;
|
||||||
|
|
||||||
|
type UUID = String;
|
||||||
|
|
||||||
|
#[derive(GraphQLQuery)]
|
||||||
|
#[graphql(
|
||||||
|
schema_path = "graphql/schema.graphql",
|
||||||
|
query_path = "graphql/mutations/revoke_token.graphql",
|
||||||
|
response_derives = "Debug",
|
||||||
|
skip_serializing_none
|
||||||
|
)]
|
||||||
|
pub struct RevokeToken;
|
|
@ -4,6 +4,7 @@ use graphql_client::GraphQLQuery;
|
||||||
#[graphql(
|
#[graphql(
|
||||||
schema_path = "graphql/schema.graphql",
|
schema_path = "graphql/schema.graphql",
|
||||||
query_path = "graphql/mutations/vote.graphql",
|
query_path = "graphql/mutations/vote.graphql",
|
||||||
response_derives = "Debug"
|
response_derives = "Debug",
|
||||||
|
skip_serializing_none
|
||||||
)]
|
)]
|
||||||
pub struct Vote;
|
pub struct Vote;
|
||||||
|
|
|
@ -4,6 +4,7 @@ use graphql_client::GraphQLQuery;
|
||||||
#[graphql(
|
#[graphql(
|
||||||
schema_path = "graphql/schema.graphql",
|
schema_path = "graphql/schema.graphql",
|
||||||
query_path = "graphql/queries/config.graphql",
|
query_path = "graphql/queries/config.graphql",
|
||||||
response_derives = "Debug"
|
response_derives = "Debug",
|
||||||
|
skip_serializing_none
|
||||||
)]
|
)]
|
||||||
pub struct Config;
|
pub struct Config;
|
||||||
|
|
|
@ -4,6 +4,7 @@ use graphql_client::GraphQLQuery;
|
||||||
#[graphql(
|
#[graphql(
|
||||||
schema_path = "graphql/schema.graphql",
|
schema_path = "graphql/schema.graphql",
|
||||||
query_path = "graphql/queries/me.graphql",
|
query_path = "graphql/queries/me.graphql",
|
||||||
response_derives = "Debug"
|
response_derives = "Debug",
|
||||||
|
skip_serializing_none
|
||||||
)]
|
)]
|
||||||
pub struct Me;
|
pub struct Me;
|
||||||
|
|
|
@ -3,3 +3,4 @@ pub mod me;
|
||||||
pub mod ok;
|
pub mod ok;
|
||||||
pub mod students_can_vote;
|
pub mod students_can_vote;
|
||||||
pub mod teachers;
|
pub mod teachers;
|
||||||
|
pub mod tokens;
|
||||||
|
|
|
@ -4,6 +4,7 @@ use graphql_client::GraphQLQuery;
|
||||||
#[graphql(
|
#[graphql(
|
||||||
schema_path = "graphql/schema.graphql",
|
schema_path = "graphql/schema.graphql",
|
||||||
query_path = "graphql/queries/ok.graphql",
|
query_path = "graphql/queries/ok.graphql",
|
||||||
response_derives = "Debug"
|
response_derives = "Debug",
|
||||||
|
skip_serializing_none
|
||||||
)]
|
)]
|
||||||
pub struct Ok;
|
pub struct Ok;
|
||||||
|
|
|
@ -4,6 +4,7 @@ use graphql_client::GraphQLQuery;
|
||||||
#[graphql(
|
#[graphql(
|
||||||
schema_path = "graphql/schema.graphql",
|
schema_path = "graphql/schema.graphql",
|
||||||
query_path = "graphql/queries/students_can_vote.graphql",
|
query_path = "graphql/queries/students_can_vote.graphql",
|
||||||
response_derives = "Debug"
|
response_derives = "Debug",
|
||||||
|
skip_serializing_none
|
||||||
)]
|
)]
|
||||||
pub struct StudentsCanVote;
|
pub struct StudentsCanVote;
|
||||||
|
|
|
@ -4,6 +4,7 @@ use graphql_client::GraphQLQuery;
|
||||||
#[graphql(
|
#[graphql(
|
||||||
schema_path = "graphql/schema.graphql",
|
schema_path = "graphql/schema.graphql",
|
||||||
query_path = "graphql/queries/teachers.graphql",
|
query_path = "graphql/queries/teachers.graphql",
|
||||||
response_derives = "Debug"
|
response_derives = "Debug",
|
||||||
|
skip_serializing_none
|
||||||
)]
|
)]
|
||||||
pub struct Teachers;
|
pub struct Teachers;
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
use graphql_client::GraphQLQuery;
|
||||||
|
|
||||||
|
type UUID = String;
|
||||||
|
type Time = String;
|
||||||
|
|
||||||
|
#[derive(GraphQLQuery)]
|
||||||
|
#[graphql(
|
||||||
|
schema_path = "graphql/schema.graphql",
|
||||||
|
query_path = "graphql/queries/tokens.graphql",
|
||||||
|
response_derives = "Debug"
|
||||||
|
)]
|
||||||
|
pub struct Tokens;
|
|
@ -1,17 +1,21 @@
|
||||||
|
use graphql_client::reqwest::post_graphql;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use yew_agent::{Dispatched, Dispatcher};
|
use yew_agent::{Dispatched, Dispatcher};
|
||||||
use yew_router::prelude::*;
|
use yew_router::prelude::*;
|
||||||
|
|
||||||
use crate::agents;
|
use crate::agents;
|
||||||
use crate::components;
|
use crate::components;
|
||||||
|
use crate::graphql;
|
||||||
use crate::routes;
|
use crate::routes;
|
||||||
|
|
||||||
pub enum Msg {
|
pub enum Msg {
|
||||||
LogOut,
|
LogOutClicked,
|
||||||
|
LogOut(Option<Vec<String>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct MainProps {
|
pub struct MainProps {
|
||||||
|
pub token: Option<String>,
|
||||||
pub logged_in: bool,
|
pub logged_in: bool,
|
||||||
#[prop_or_default]
|
#[prop_or_default]
|
||||||
pub children: Children,
|
pub children: Children,
|
||||||
|
@ -20,6 +24,7 @@ pub struct MainProps {
|
||||||
pub struct Main {
|
pub struct Main {
|
||||||
logged_in: bool,
|
logged_in: bool,
|
||||||
logged_in_event_bus: Dispatcher<agents::logged_in::EventBus>,
|
logged_in_event_bus: Dispatcher<agents::logged_in::EventBus>,
|
||||||
|
errors: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for Main {
|
impl Component for Main {
|
||||||
|
@ -30,50 +35,75 @@ impl Component for Main {
|
||||||
Self {
|
Self {
|
||||||
logged_in: ctx.props().logged_in,
|
logged_in: ctx.props().logged_in,
|
||||||
logged_in_event_bus: agents::logged_in::EventBus::dispatcher(),
|
logged_in_event_bus: agents::logged_in::EventBus::dispatcher(),
|
||||||
|
errors: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||||
match msg {
|
match msg {
|
||||||
Msg::LogOut => {
|
Msg::LogOutClicked => {
|
||||||
self.logged_in_event_bus
|
let client = graphql::client(ctx.props().token.as_ref()).unwrap();
|
||||||
.send(agents::logged_in::Request::LoggedIn(false));
|
ctx.link().send_future(async move {
|
||||||
|
let response = post_graphql::<graphql::mutations::logout::Logout, _>(
|
||||||
|
&client,
|
||||||
|
graphql::URL.as_str(),
|
||||||
|
graphql::mutations::logout::logout::Variables,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Msg::LogOut(components::graphql_errors::convert(response.errors))
|
||||||
|
});
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
Msg::LogOut(errors) => {
|
||||||
|
if self.errors.is_none() {
|
||||||
|
self.logged_in_event_bus
|
||||||
|
.send(agents::logged_in::Request::LoggedIn(false));
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
self.errors = errors;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
let logout_onclick = ctx.link().callback(move |_| Msg::LogOut);
|
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
<nav>
|
<components::logged_in_handler::LoggedInHandler logged_in={self.logged_in} />
|
||||||
<ul>
|
<nav>
|
||||||
<li>
|
<ul>
|
||||||
<Link<routes::Route> to={routes::Route::Home}>
|
<li>
|
||||||
<button>{ "Home" }</button>
|
<Link<routes::Route> to={routes::Route::Home}>
|
||||||
</Link<routes::Route>>
|
<button>{ "Home" }</button>
|
||||||
</li>
|
</Link<routes::Route>>
|
||||||
<li>
|
</li>
|
||||||
if self.logged_in {
|
<li>
|
||||||
<button onclick={logout_onclick}>{ "Logout" }</button>
|
<Link<routes::Route> to={routes::Route::Settings}>
|
||||||
} else {
|
<button>{ "Settings" }</button>
|
||||||
<Link<routes::Route> to={routes::Route::Login}>
|
</Link<routes::Route>>
|
||||||
<button>{ "Login" }</button>
|
</li>
|
||||||
</Link<routes::Route>>
|
<li>
|
||||||
}
|
if self.logged_in {
|
||||||
</li>
|
<button onclick={ctx.link().callback(move |_| Msg::LogOutClicked)}>{ "Logout" }</button>
|
||||||
</ul>
|
} else {
|
||||||
</nav>
|
<Link<routes::Route> to={routes::Route::Login}>
|
||||||
|
<button>{ "Login" }</button>
|
||||||
|
</Link<routes::Route>>
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<hr />
|
<components::graphql_errors::GraphQLErrors errors={self.errors.clone()} />
|
||||||
|
</nav>
|
||||||
<components::logged_in_handler::LoggedInHandler logged_in={self.logged_in} />
|
<hr />
|
||||||
<main>
|
<main>
|
||||||
{ for ctx.props().children.iter() }
|
{ for ctx.props().children.iter() }
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,13 +22,8 @@ pub struct HomeProps {
|
||||||
pub token: Option<String>,
|
pub token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum State {
|
|
||||||
Fetching,
|
|
||||||
Done,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Home {
|
pub struct Home {
|
||||||
state: State,
|
fetching: bool,
|
||||||
errors: Option<Vec<String>>,
|
errors: Option<Vec<String>>,
|
||||||
me: Option<graphql::queries::me::me::MeMe>,
|
me: Option<graphql::queries::me::me::MeMe>,
|
||||||
}
|
}
|
||||||
|
@ -37,9 +32,25 @@ impl Component for Home {
|
||||||
type Message = Msg;
|
type Message = Msg;
|
||||||
type Properties = HomeProps;
|
type Properties = HomeProps;
|
||||||
|
|
||||||
fn create(_ctx: &Context<Self>) -> Self {
|
fn create(ctx: &Context<Self>) -> Self {
|
||||||
|
let client = graphql::client(ctx.props().token.as_ref()).unwrap();
|
||||||
|
ctx.link().send_future(async move {
|
||||||
|
let response = post_graphql::<graphql::queries::me::Me, _>(
|
||||||
|
&client,
|
||||||
|
graphql::URL.as_str(),
|
||||||
|
graphql::queries::me::me::Variables,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Msg::DoneFetching {
|
||||||
|
errors: components::graphql_errors::convert(response.errors),
|
||||||
|
me: response.data.unwrap().me.unwrap(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
state: State::Fetching,
|
fetching: true,
|
||||||
errors: None,
|
errors: None,
|
||||||
me: None,
|
me: None,
|
||||||
}
|
}
|
||||||
|
@ -48,7 +59,7 @@ impl Component for Home {
|
||||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||||
match msg {
|
match msg {
|
||||||
Msg::DoneFetching { errors, me } => {
|
Msg::DoneFetching { errors, me } => {
|
||||||
self.state = State::Done;
|
self.fetching = false;
|
||||||
self.errors = errors;
|
self.errors = errors;
|
||||||
self.me = Some(me);
|
self.me = Some(me);
|
||||||
true
|
true
|
||||||
|
@ -60,62 +71,39 @@ impl Component for Home {
|
||||||
html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
<Title value="Home" />
|
<Title value="Home" />
|
||||||
{
|
if self.fetching {
|
||||||
match self.state {
|
<p>{ "Fetching..." }</p>
|
||||||
State::Fetching => {
|
} else {
|
||||||
let client = graphql::client(ctx.props().token.as_ref()).unwrap();
|
if let Some(errors) = &self.errors {
|
||||||
ctx.link().send_future(async move {
|
<components::graphql_errors::GraphQLErrors errors={errors.clone()} />
|
||||||
let response = post_graphql::<graphql::queries::me::Me, _>(
|
} else {
|
||||||
&client,
|
{{
|
||||||
graphql::URL.as_str(),
|
let me = self.me.as_ref().unwrap();
|
||||||
graphql::queries::me::me::Variables,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
Msg::DoneFetching {
|
|
||||||
errors: components::graphql_errors::convert(response.errors),
|
|
||||||
me: response.data.unwrap().me.unwrap(),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<p>{ "Fetching..." }</p>
|
<>
|
||||||
}
|
<h1>{ format!("Hey, {}!", me.first_name) }</h1>
|
||||||
}
|
<hr />
|
||||||
State::Done => {
|
{
|
||||||
if let Some(errors) = &self.errors {
|
match me.role {
|
||||||
html! {
|
graphql::queries::me::me::UserRole::Student => html! {
|
||||||
<components::graphql_errors::GraphQLErrors errors={errors.clone()} />
|
<student_home::StudentHome
|
||||||
}
|
token={ctx.props().token.as_ref().unwrap().to_owned()}
|
||||||
} else {
|
voted={me.student.as_ref().unwrap().vote.is_some()}
|
||||||
let me = self.me.as_ref().unwrap();
|
/>
|
||||||
|
},
|
||||||
html! {
|
graphql::queries::me::me::UserRole::Teacher => html! {
|
||||||
<>
|
<teacher_home::TeacherHome
|
||||||
<h1>{ format!("Hey, {}!", me.first_name) }</h1>
|
token={ctx.props().token.as_ref().unwrap().to_owned()}
|
||||||
<hr />
|
registered={me.teacher.is_some()}
|
||||||
{
|
/>
|
||||||
match me.role {
|
},
|
||||||
graphql::queries::me::me::UserRole::Student => html! {
|
graphql::queries::me::me::UserRole::Other(_) => html! {}
|
||||||
<student_home::StudentHome
|
|
||||||
token={ctx.props().token.as_ref().unwrap().to_owned()}
|
|
||||||
voted={me.student.as_ref().unwrap().vote.is_some()}
|
|
||||||
/>
|
|
||||||
},
|
|
||||||
graphql::queries::me::me::UserRole::Teacher => html! {
|
|
||||||
<teacher_home::TeacherHome
|
|
||||||
token={ctx.props().token.as_ref().unwrap().to_owned()}
|
|
||||||
registered={me.teacher.is_some()}
|
|
||||||
/>
|
|
||||||
},
|
|
||||||
graphql::queries::me::me::UserRole::Other(_) => html! {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</>
|
}
|
||||||
}
|
</>
|
||||||
}
|
}
|
||||||
}
|
}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -231,10 +231,8 @@ impl Component for StudentVote {
|
||||||
<form {onsubmit}>
|
<form {onsubmit}>
|
||||||
{ for (0..self.slots).map(|i| {
|
{ for (0..self.slots).map(|i| {
|
||||||
let curr_t = self.votes.get(&i);
|
let curr_t = self.votes.get(&i);
|
||||||
if let Some(te) = curr_t {
|
if let Some(Some(t)) = curr_t {
|
||||||
if let Some(t) = te {
|
teachers.push(*t);
|
||||||
teachers.push(*t);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let name = format!("mentors_{}", i);
|
let name = format!("mentors_{}", i);
|
||||||
|
|
||||||
|
|
|
@ -87,7 +87,7 @@ impl Component for TeacherRegistration {
|
||||||
<label for="max_students">
|
<label for="max_students">
|
||||||
{ "Maximale Anzahl von Schülern:" }
|
{ "Maximale Anzahl von Schülern:" }
|
||||||
<br />
|
<br />
|
||||||
<input ref={self.max_students.clone()} type="number" id="max_students" name="max_students" min=1 />
|
<input ref={self.max_students.clone()} type="number" id="max_students" name="max_students" min=0 />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -7,11 +7,14 @@ use crate::layouts;
|
||||||
pub mod home;
|
pub mod home;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
pub mod not_found;
|
pub mod not_found;
|
||||||
|
pub mod settings;
|
||||||
|
|
||||||
#[derive(Clone, Routable, PartialEq, Eq)]
|
#[derive(Clone, Routable, PartialEq, Eq)]
|
||||||
pub enum Route {
|
pub enum Route {
|
||||||
#[at("/")]
|
#[at("/")]
|
||||||
Home,
|
Home,
|
||||||
|
#[at("/settings")]
|
||||||
|
Settings,
|
||||||
#[at("/login")]
|
#[at("/login")]
|
||||||
Login,
|
Login,
|
||||||
#[not_found]
|
#[not_found]
|
||||||
|
@ -39,19 +42,28 @@ pub fn switch(routes: &Route) -> Html {
|
||||||
Route::Home => {
|
Route::Home => {
|
||||||
html! {
|
html! {
|
||||||
<layouts::logged_in::LoggedIn {logged_in}>
|
<layouts::logged_in::LoggedIn {logged_in}>
|
||||||
<layouts::main::Main {logged_in}>
|
<layouts::main::Main token={token.to_owned()} {logged_in}>
|
||||||
<home::Home {token} />
|
<home::Home {token} />
|
||||||
</layouts::main::Main>
|
</layouts::main::Main>
|
||||||
</layouts::logged_in::LoggedIn>
|
</layouts::logged_in::LoggedIn>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Route::Settings => {
|
||||||
|
html! {
|
||||||
|
<layouts::logged_in::LoggedIn {logged_in}>
|
||||||
|
<layouts::main::Main token={token.to_owned()} {logged_in}>
|
||||||
|
<settings::Settings {token} />
|
||||||
|
</layouts::main::Main>
|
||||||
|
</layouts::logged_in::LoggedIn>
|
||||||
|
}
|
||||||
|
}
|
||||||
Route::Login => html! {
|
Route::Login => html! {
|
||||||
<layouts::main::Main {logged_in}>
|
<layouts::main::Main {token} {logged_in}>
|
||||||
<login::Login />
|
<login::Login />
|
||||||
</layouts::main::Main>
|
</layouts::main::Main>
|
||||||
},
|
},
|
||||||
Route::NotFound => html! {
|
Route::NotFound => html! {
|
||||||
<layouts::main::Main {logged_in}>
|
<layouts::main::Main {token} {logged_in}>
|
||||||
<not_found::NotFound />
|
<not_found::NotFound />
|
||||||
</layouts::main::Main>
|
</layouts::main::Main>
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
use yew::prelude::*;
|
||||||
|
use yew_side_effect::title::Title;
|
||||||
|
|
||||||
|
pub mod tokens;
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq, Eq)]
|
||||||
|
pub struct SettingsProps {
|
||||||
|
pub token: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Settings;
|
||||||
|
|
||||||
|
impl Component for Settings {
|
||||||
|
type Message = ();
|
||||||
|
type Properties = SettingsProps;
|
||||||
|
|
||||||
|
fn create(_ctx: &Context<Self>) -> Self {
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<Title value="Settings" />
|
||||||
|
<section>
|
||||||
|
<tokens::Tokens token={ctx.props().token.as_ref().unwrap().to_owned()} />
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
use graphql_client::reqwest::post_graphql;
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
use crate::components;
|
||||||
|
use crate::graphql;
|
||||||
|
|
||||||
|
pub enum Msg {
|
||||||
|
DoneFetching {
|
||||||
|
errors: Option<Vec<String>>,
|
||||||
|
tokens: Vec<graphql::queries::tokens::tokens::TokensTokens>,
|
||||||
|
},
|
||||||
|
Revoke(usize),
|
||||||
|
RevokeDone {
|
||||||
|
errors: Option<Vec<String>>,
|
||||||
|
id: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq, Eq)]
|
||||||
|
pub struct TokensProps {
|
||||||
|
pub token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Tokens {
|
||||||
|
fetching: bool,
|
||||||
|
errors: Option<Vec<String>>,
|
||||||
|
tokens: Option<Vec<graphql::queries::tokens::tokens::TokensTokens>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for Tokens {
|
||||||
|
type Message = Msg;
|
||||||
|
type Properties = TokensProps;
|
||||||
|
|
||||||
|
fn create(ctx: &Context<Self>) -> Self {
|
||||||
|
let client = graphql::client(Some(&ctx.props().token)).unwrap();
|
||||||
|
ctx.link().send_future(async move {
|
||||||
|
let response = post_graphql::<graphql::queries::tokens::Tokens, _>(
|
||||||
|
&client,
|
||||||
|
graphql::URL.as_str(),
|
||||||
|
graphql::queries::tokens::tokens::Variables,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Msg::DoneFetching {
|
||||||
|
errors: None,
|
||||||
|
tokens: response.data.unwrap().tokens.unwrap(),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
fetching: true,
|
||||||
|
errors: None,
|
||||||
|
tokens: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||||
|
match msg {
|
||||||
|
Msg::DoneFetching { errors, tokens } => {
|
||||||
|
self.fetching = false;
|
||||||
|
self.errors = errors;
|
||||||
|
self.tokens = Some(tokens);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Msg::Revoke(id) => {
|
||||||
|
let client = graphql::client(Some(&ctx.props().token)).unwrap();
|
||||||
|
let token = self.tokens.as_ref().unwrap()[id].id.to_owned();
|
||||||
|
ctx.link().send_future(async move {
|
||||||
|
let response =
|
||||||
|
post_graphql::<graphql::mutations::revoke_token::RevokeToken, _>(
|
||||||
|
&client,
|
||||||
|
graphql::URL.as_str(),
|
||||||
|
graphql::mutations::revoke_token::revoke_token::Variables { token },
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Msg::RevokeDone {
|
||||||
|
errors: components::graphql_errors::convert(response.errors),
|
||||||
|
id,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Msg::RevokeDone { errors, id } => {
|
||||||
|
self.errors = errors;
|
||||||
|
self.tokens.as_mut().unwrap().remove(id);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
|
html! {
|
||||||
|
<fieldset>
|
||||||
|
<legend>{ "Tokens" }</legend>
|
||||||
|
if self.fetching {
|
||||||
|
<p>{ "Fetching..." }</p>
|
||||||
|
} else {
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>{ "ID" }</th>
|
||||||
|
<th>{ "Issued at" }</th>
|
||||||
|
<th>{ "Expires at" }</th>
|
||||||
|
<th>{ "Revoke" }</th>
|
||||||
|
</tr>
|
||||||
|
{ for self.tokens.as_ref().unwrap().iter().enumerate().map(|(i, t)| {
|
||||||
|
html! {
|
||||||
|
<tr>
|
||||||
|
<td><code>{ &t.id }</code></td>
|
||||||
|
<td><code>{ &t.iat }</code></td>
|
||||||
|
<td><code>{ &t.exp }</code></td>
|
||||||
|
<td>
|
||||||
|
<button onclick={ctx.link().callback(move |_| Msg::Revoke(i))}>
|
||||||
|
{ "Revoke" }
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}) }
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<components::graphql_errors::GraphQLErrors errors={self.errors.clone()} />
|
||||||
|
}
|
||||||
|
</fieldset>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue