Merge pull request 'LDAP' (#28) from ldap into main
All checks were successful
continuous-integration/drone/push Build is passing

Reviewed-on: #28
This commit is contained in:
Dominic Grimm 2022-02-06 18:50:29 +00:00
commit 6cbfd285d8
34 changed files with 271 additions and 386 deletions

View file

@ -13,9 +13,16 @@ BACKEND_JWT_SECRET=
# Backend - Worker
# Backend - SMTP
BACKEND_SMTP_HELO=
BACKEND_SMTP_ADDRESS=
BACKEND_SMTP_HOST=
BACKEND_SMTP_PORT=
BACKEND_SMTP_NAME=
BACKEND_SMTP_USERNAME=
BACKEND_SMTP_PASSWORD=
# Backend - Db
# Backend - LDAP
BACKEND_LDAP_HOST=
BACKEND_LDAP_PORT=
BACKEND_LDAP_BASE_DN=
BACKEND_BIND_DN=
BACKEND_BIND_PASSWORD=
BACKEND_LDAP_USER_DN=

View file

@ -1,3 +1,5 @@
version: "3"
services:
nginx:
image: nginx:alpine
@ -63,12 +65,18 @@ services:
BACKEND_API_JWT_SECRET: ${BACKEND_JWT_SECRET}
BACKEND_WORKER_REDIS_URL: redis://redis:6379
BACKEND_SMTP_HELO: ${BACKEND_SMTP_HELO}
BACKEND_SMTP_ADDRESS: ${BACKEND_SMTP_ADDRESS}
BACKEND_SMTP_HOST: ${BACKEND_SMTP_HOST}
BACKEND_SMTP_PORT: ${BACKEND_SMTP_PORT}
BACKEND_SMTP_NAME: ${BACKEND_SMTP_NAME}
BACKEND_SMTP_USERNAME: ${BACKEND_SMTP_USERNAME}
BACKEND_SMTP_PASSWORD: ${BACKEND_SMTP_PASSWORD}
BACKEND_DB_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_USER}
BACKEND_LDAP_HOST: ${BACKEND_LDAP_HOST}
BACKEND_LDAP_PORT: ${BACKEND_LDAP_PORT}
BACKEND_LDAP_BASE_DN: ${BACKEND_LDAP_BASE_DN}
BACKEND_LDAP_BIND_DN: ${BACKEND_LDAP_BIND_DN}
BACKEND_LDAP_BIND_PASSWORD: ${BACKEND_LDAP_BIND_PASSWORD}
BACKEND_LDAP_USER_DN: ${BACKEND_LDAP_USER_DN}
frontend:
build:

View file

@ -1,15 +0,0 @@
-- +micrate Up
-- SQL in section 'Up' is executed when this migration is applied
CREATE TABLE users(
id BIGSERIAL PRIMARY KEY,
firstname TEXT NOT NULL,
lastname TEXT NOT NULL,
email TEXT NOT NULL,
PASSWORD TEXT NOT NULL,
blocked BOOLEAN NOT NULL,
UNIQUE (firstname, lastname, email)
);
-- +micrate Down
-- SQL section 'Down' is executed when this migration is rolled back
DROP TABLE users;

View file

@ -1,15 +0,0 @@
-- +micrate Up
-- SQL in section 'Up' is executed when this migration is applied
CREATE TYPE user_roles AS ENUM ('Admin', 'Teacher', 'Student');
ALTER TABLE
users
ADD
COLUMN role user_roles NOT NULL;
-- +micrate Down
-- SQL section 'Down' is executed when this migration is rolled back
ALTER TABLE
users DROP COLUMN role;
DROP TYPE user_roles;

View file

@ -1,27 +0,0 @@
-- +micrate Up
-- SQL in section 'Up' is executed when this migration is applied
CREATE TABLE votes(
id BIGSERIAL PRIMARY KEY,
student_id BIGINT NOT NULL UNIQUE REFERENCES students(id)
);
ALTER TABLE
students
ADD
COLUMN vote_id BIGINT UNIQUE REFERENCES votes(id);
CREATE TABLE teacher_votes(
id BIGSERIAL PRIMARY KEY,
vote_id BIGINT NOT NULL REFERENCES votes(id),
teacher_id BIGINT NOT NULL REFERENCES teachers(id),
priority INT NOT NULL
);
-- +micrate Down
-- SQL section 'Down' is executed when this migration is rolled back
ALTER TABLE
students DROP COLUMN vote_id;
DROP TABLE teacher_votes;
DROP TABLE votes;

View file

@ -1,8 +1,12 @@
-- +micrate Up
-- SQL in section 'Up' is executed when this migration is applied
CREATE TABLE admins(
CREATE TYPE user_roles AS ENUM ('Teacher', 'Student');
CREATE TABLE users(
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL UNIQUE REFERENCES users(id)
username TEXT UNIQUE NOT NULL,
role user_roles NOT NULL,
admin BOOLEAN NOT NULL
);
CREATE TABLE teachers(
@ -18,11 +22,6 @@ CREATE TABLE students(
skif BOOLEAN NOT NULL
);
ALTER TABLE
users
ADD
COLUMN admin_id BIGINT UNIQUE REFERENCES admins(id);
ALTER TABLE
users
ADD
@ -33,19 +32,35 @@ ALTER TABLE
ADD
COLUMN student_id BIGINT UNIQUE REFERENCES students(id);
CREATE TABLE votes(
id BIGSERIAL PRIMARY KEY,
student_id BIGINT NOT NULL UNIQUE REFERENCES students(id)
);
ALTER TABLE
students
ADD
COLUMN vote_id BIGINT UNIQUE REFERENCES votes(id);
CREATE TABLE teacher_votes(
id BIGSERIAL PRIMARY KEY,
vote_id BIGINT NOT NULL REFERENCES votes(id),
teacher_id BIGINT NOT NULL REFERENCES teachers(id),
priority INT NOT NULL
);
-- +micrate Down
-- SQL section 'Down' is executed when this migration is rolled back
ALTER TABLE
users DROP COLUMN admin_id;
DROP TABLE teacher_votes;
ALTER TABLE
users DROP COLUMN teacher_id;
ALTER TABLE
users DROP COLUMN student_id;
DROP TABLE votes;
DROP TABLE admins;
DROP TABLE teachers;
DROP TABLE students;
DROP TABLE students;
DROP TABLE users;
DROP TYPE user_roles;

View file

@ -1,9 +1,5 @@
version: 2.0
shards:
CrystalEmail:
git: https://git.sceptique.eu/Sceptique/CrystalEmail.git
version: 0.2.6+git.commit.f217992c51048b3f94f4e064cd6c5123e32a1e27
bindata:
git: https://github.com/spider-gazelle/bindata.git
version: 1.9.1
@ -12,14 +8,6 @@ shards:
git: https://github.com/mrrooijen/commander.git
version: 0.4.0
crystal-argon2:
git: https://github.com/axentro/crystal-argon2.git
version: 0.1.3
cute:
git: https://github.com/papierkorb/cute.git
version: 0.4.0
db:
git: https://github.com/crystal-lang/crystal-db.git
version: 0.10.1
@ -33,17 +21,9 @@ shards:
version: 0.6.3
env_config:
git: https://github.com/jreinert/env_config.cr.git
git: https://github.com/repomaa/env_config.cr.git
version: 0.1.0+git.commit.a3ef5b955f27e2c65de2fe0ff41718e2eea7c06f
fancyline:
git: https://github.com/papierkorb/fancyline.git
version: 0.4.1
future:
git: https://github.com/crystal-community/future.cr.git
version: 1.0.0
granite:
git: https://github.com/amberframework/granite.git
version: 0.23.0
@ -68,6 +48,14 @@ shards:
git: https://github.com/jeromegn/kilt.git
version: 0.6.1
ldap:
git: https://github.com/spider-gazelle/crystal-ldap.git
version: 0.9.1
ldap_escape:
git: https://git.dergrimm.net/dergrimm/ldap_escape.git
version: 0.1.0
micrate:
git: https://github.com/juanedi/micrate.git
version: 0.12.0
@ -88,6 +76,10 @@ shards:
git: https://github.com/ysbaddaden/pool.git
version: 0.2.4
promise:
git: https://github.com/spider-gazelle/promise.git
version: 3.0.0
quartz_mailer:
git: https://github.com/amberframework/quartz-mailer.git
version: 0.8.0

View file

@ -19,20 +19,13 @@ dependencies:
github: amberframework/granite
pg:
github: will/crystal-pg
crystal-argon2:
github: Axentro/crystal-argon2
graphql:
github: graphql-crystal/graphql
branch: master
jwt:
github: crystal-community/jwt
CrystalEmail:
git: https://git.sceptique.eu/Sceptique/CrystalEmail.git
branch: master
commander:
github: mrrooijen/commander
fancyline:
github: Papierkorb/fancyline
micrate:
github: juanedi/micrate
mosquito:
@ -52,4 +45,8 @@ dependencies:
html-minifier:
github: sam0x17/html-minifier
env_config:
github: jreinert/env_config.cr
github: repomaa/env_config.cr
ldap:
github: spider-gazelle/crystal-ldap
ldap_escape:
git: https://git.dergrimm.net/dergrimm/ldap_escape.git

View file

@ -1,28 +1,17 @@
require "crystal-argon2"
require "jwt"
module Backend
module API
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
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 = (Time.utc + Time::Span.new(days: 1)).to_unix) : String
def create_user_jwt(user_id : Int, expiration : Int) : String
create_jwt({user_id: user_id}, expiration)
end

View file

@ -3,14 +3,15 @@ require "graphql"
require "granite"
module Backend
module API
module Api
class Context < GraphQL::Context
getter user : Db::User?
getter admin : Bool?
getter role : Schema::UserRole?
getter external : (Db::Admin | Db::Teacher | Db::Student)?
getter external : (Db::Teacher | Db::Student)?
def initialize(request : HTTP::Request, *rest)
super(*rest)
def initialize(request : HTTP::Request, max_complexity : Int32? = nil)
super(max_complexity)
token = request.headers["Authorization"]?
if token && token[..Auth::BEARER.size - 1] == Auth::BEARER
@ -19,14 +20,13 @@ module Backend
data = payload["data"].as_h
@user = Db::User.find(data["user_id"].as_i)
return if @user.nil? || @user.not_nil!.blocked
return unless @user
if @user
@admin = @user.not_nil!.admin
@role = Schema::UserRole.parse(@user.not_nil!.role).not_nil!
@external =
case @role.not_nil!
when .admin?
@user.not_nil!.admin
when .teacher?
@user.not_nil!.teacher
when .student?
@ -37,7 +37,7 @@ module Backend
end
def authenticated? : Bool
!@user.nil?
!!@user
end
def authenticated! : Bool
@ -46,14 +46,22 @@ module Backend
true
end
def admin? : Bool
authenticated? && !!@admin
end
def admin! : Bool
raise "Invalid permissions" unless admin?
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
role == case @external.not_nil!
when Db::Teacher
Schema::UserRole::Teacher
when Db::Student
@ -73,32 +81,45 @@ module Backend
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
def teacher?(external = false) : Bool
role? external, Schema::UserRole::Teacher
end
def teacher! : Bool
role! external, Schema::UserRole::Teacher
end
def student?(external = false) : Bool
role? external, Schema::UserRole::Student
end
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

@ -1,7 +1,7 @@
require "log"
module Backend
module API
module Api
Log = ::Log.for(self)
end
end

View file

@ -1,7 +1,7 @@
require "http/server"
module Backend
module API
module Api
extend self
def run : Nil

View file

@ -4,7 +4,7 @@ require "./schema/helpers"
require "./schema/*"
module Backend
module API
module Api
module Schema
SCHEMA = GraphQL::Schema.new(Query.new, Mutation.new)
end

View file

@ -1,26 +0,0 @@
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

View file

@ -1,5 +1,5 @@
module Backend
module API
module Api
module Schema
module Helpers
module ObjectMacros
@ -38,8 +38,8 @@ module Backend
module DbObject
macro db_object(type)
include ::Backend::API::Schema::Helpers::ObjectDbInit
include ::Backend::API::Schema::Helpers::ObjectFinders
include ::Backend::Api::Schema::Helpers::ObjectDbInit
include ::Backend::Api::Schema::Helpers::ObjectFinders
db_init {{ type }}
finders {{ type }}

View file

@ -1,51 +1,36 @@
require "CrystalEmail"
require "ldap"
module Backend
module API
module Api
module Schema
@[GraphQL::Object]
class Mutation < GraphQL::BaseMutation
@[GraphQL::Field]
def login(email : String, password : String) : LoginPayload
raise "Auth failed" if email.empty? || password.empty? || !CrystalEmail::Rfc5322::Public.validates?(email)
def login(username : String, password : String) : LoginPayload
raise "Auth failed" if username.empty? || password.empty?
user = Db::User.find_by(email: email)
raise "Auth failed" unless user && Auth.verify_password?(password, user.password)
user = Db::User.find_by(username: username)
raise "Auth failed" unless user && Ldap.authenticate?(Ldap.uid(username), password)
LoginPayload.new(
user: User.new(user),
token: Auth.create_user_jwt(user.id.not_nil!.to_i),
token: Auth.create_user_jwt(
user.id.not_nil!.to_i,
(Time.utc + (user.admin ? Time::Span.new(hours: 6) : Time::Span.new(days: 1))).to_unix
),
)
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
def create_user(context : Context, input : UserCreateInput, check_ldap : Bool = true) : 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,
)
raise "LDAP user does not exist" if check_ldap && begin
!Ldap.user(Ldap.uid(input.username))
rescue LDAP::Client::AuthError
true
end
user = Db::User.create!(username: input.username, role: input.role.to_s)
User.new(user)
end
@ -60,24 +45,6 @@ module Backend
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 send_teachers_registration_email(context : Context) : Bool
context.admin!

View file

@ -1,5 +1,5 @@
module Backend
module API
module Api
module Schema
@[GraphQL::Object]
class Query < GraphQL::BaseQuery
@ -30,17 +30,10 @@ module Backend
end
@[GraphQL::Field]
def admin(context : Context, id : Int32) : Admin
def admins(context : Context) : Array(User)
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) }
Db::User.where(admin: true).map { |user| User.new(user) }
end
@[GraphQL::Field]

View file

@ -1,5 +1,5 @@
module Backend
module API
module Api
module Schema
@[GraphQL::Object]
class Student < GraphQL::BaseObject

View file

@ -1,5 +1,5 @@
module Backend
module API
module Api
module Schema
@[GraphQL::Object]
class Teacher < GraphQL::BaseObject

View file

@ -1,5 +1,5 @@
module Backend
module API
module Api
module Schema
@[GraphQL::Object]
class TeacherVote < GraphQL::BaseObject

View file

@ -1,5 +1,5 @@
module Backend
module API
module Api
module Schema
@[GraphQL::Object]
class User < GraphQL::BaseObject
@ -17,20 +17,33 @@ module Backend
find!.lastname
end
@[GraphQL::Field]
def name : String
find!.name
end
@[GraphQL::Field]
def username : String
find!.username
end
@[GraphQL::Field]
def email : String
find!.email
end
@[GraphQL::Field]
def admin : Bool
find!.admin
end
@[GraphQL::Field]
def role : UserRole
role = Db::UserRole.parse(find!.role)
case role
when Db::UserRole::Admin
UserRole::Admin
when Db::UserRole::Teacher
when .teacher?
UserRole::Teacher
when Db::UserRole::Student
when .student?
UserRole::Student
else
raise "Unknown role: #{role}"
@ -40,8 +53,6 @@ module Backend
@[GraphQL::Field]
def external_id : Int32?
case Db::UserRole.parse(find!.role)
when .admin?
find!.admin
when .teacher?
find!.teacher
when .student?
@ -51,14 +62,6 @@ module Backend
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
@ -74,47 +77,21 @@ module Backend
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 username
getter role
getter blocked
@[GraphQL::Field]
def initialize(
@firstname : String,
@lastname : String,
@email : String,
@password : String,
@role : UserRole,
@blocked : Bool = false
@username : String,
@role : UserRole
)
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

View file

@ -1,9 +1,8 @@
module Backend
module API
module Api
module Schema
@[GraphQL::Enum]
enum UserRole
Admin
Teacher
Student
end

View file

@ -1,5 +1,5 @@
module Backend
module API
module Api
module Schema
@[GraphQL::Object]
class Vote < GraphQL::BaseObject

View file

@ -1,9 +1,9 @@
module Backend
module API
module Api
SERVICE = ->do
Log.info { "Starting API service..." }
Log.info { "Starting Api service..." }
run
Log.info { "API service stopped." }
Log.info { "Api service stopped." }
end
end
end

View file

@ -3,7 +3,7 @@ require "http/server"
require "json"
module Backend
module API
module Api
class WebServer
include Router

View file

@ -1,5 +1,4 @@
require "commander"
require "fancyline"
require "./db"
@ -7,23 +6,7 @@ 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"
@ -51,45 +34,30 @@ module Backend
end
cmd.commands.add do |c|
c.use = "seed"
c.use = "register <username> <role>"
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: ")
c.flags.add do |f|
f.name = "admin"
f.long = "--admin"
f.default = false
f.description = "Register as admin"
end
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
c.run do |opts, args|
username = args[0]
role = Db::UserRole.parse(args[1].downcase)
print "Register '#{username}' as '#{role}'#{opts.bool["admin"] ? " with admin privileges" : nil}? [y/N] "
abort unless (gets(chomp: true) || "").strip.downcase == "y"
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)
user = Db::User.create!(username: username, role: role.to_s, admin: opts.bool["admin"])
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 "User: #{user.id}"
puts "Role: #{user.role}"
puts "Admin: #{user.admin}"
puts "---"
end
end

View file

@ -33,18 +33,21 @@ module Backend
getter url : String
@[EnvConfig::Setting(key: "api")]
getter api : APIConfig
getter api : ApiConfig
@[EnvConfig::Setting(key: "worker")]
getter worker : WorkerConfig
@[EnvConfig::Setting(key: "smtp")]
getter smtp : SMTPConfig
getter smtp : SmtpConfig
@[EnvConfig::Setting(key: "db")]
getter db : DbConfig
class APIConfig
@[EnvConfig::Setting(key: "ldap")]
getter ldap : LdapConfig
class ApiConfig
include EnvConfig
getter graphql_playground : Bool
@ -61,11 +64,11 @@ module Backend
getter redis_url : String
end
class SMTPConfig
class SmtpConfig
include EnvConfig
getter helo : String
getter address : String
getter host : String
getter port : Int32
getter name : String
getter username : String
@ -77,5 +80,16 @@ module Backend
getter url : String
end
class LdapConfig
include EnvConfig
getter host : String
getter port : Int32
getter base_dn : String
getter bind_dn : String
getter bind_password : String
getter user_dn : String
end
end
end

View file

@ -1,11 +0,0 @@
module Backend
module Db
class Admin < Granite::Base
table admins
belongs_to :user
column id : Int64, primary: true
end
end
end

View file

@ -1,28 +1,30 @@
require "CrystalEmail"
module Backend
module Db
class User < Granite::Base
table users
has_one :admin
has_one :teacher
has_one :student
column id : Int64, primary: true
column firstname : String
column lastname : String
column email : String
column password : String
column username : String
column role : String
column blocked : Bool = false
column admin : Bool = false
def name : String
"#{@firstname} #{@lastname}"
def firstname : String
Ldap.user(Ldap.uid(@username.not_nil!)).first["givenName"].first
end
validate :email, "needs to be an email address" do |user|
CrystalEmail::Rfc5322::Public.validates?(user.email)
def lastname : String
Ldap.user(Ldap.uid(@username.not_nil!)).first["sn"].first
end
def name : String
Ldap.user(Ldap.uid(@username.not_nil!)).first["cn"].first
end
def email : String
Ldap.user(Ldap.uid(@username.not_nil!)).first["mail"].first
end
validate :role, "needs to be a valid role" do |user|
@ -30,16 +32,14 @@ module Backend
end
validate :role, "user external needs to be a valid role" do |user|
if user.admin.nil? && user.teacher.nil? && user.student.nil?
if user.teacher.nil? && user.student.nil?
true
else
!!case UserRole.parse(user.role)
when UserRole::Admin
user.admin && user.teacher.nil? && user.student.nil?
when UserRole::Teacher
user.admin.nil? && user.teacher && user.student.nil?
when UserRole::Student
user.admin.nil? && user.teacher.nil? && user.student
when .teacher?
user.teacher && user.student.nil?
when .student?
user.teacher.nil? && user.student
else
false
end

View file

@ -1,7 +1,6 @@
module Backend
module Db
enum UserRole
Admin
Teacher
Student
end

View file

@ -0,0 +1,33 @@
require "ldap"
require "socket"
require "ldap_escape"
module Backend
module Ldap
extend self
def create_client : LDAP::Client
LDAP::Client.new(TCPSocket.new(Backend.config.ldap.host, Backend.config.ldap.port))
end
def cn(username : String) : String
"cn=#{LdapEscape.dn(username)},#{Backend.config.ldap.user_dn}"
end
def uid(uid : String) : String
"uid=#{LdapEscape.dn(uid)},#{Backend.config.ldap.user_dn}"
end
def user(dn : String) : Array(Hash(String, Array(String)))
create_client
.authenticate(Backend.config.ldap.bind_dn, Backend.config.ldap.bind_password)
.search(base: dn)
end
def authenticate?(dn : String, password : String) : Bool
!!create_client.authenticate(dn, password)
rescue LDAP::Client::AuthError
false
end
end
end

View file

@ -8,7 +8,7 @@ module Backend
module Mailers
Quartz.config do |config|
config.smtp_enabled = true
config.smtp_address = Backend.config.smtp.address
config.smtp_address = Backend.config.smtp.host
config.smtp_port = Backend.config.smtp.port
config.helo_domain = Backend.config.smtp.helo
config.use_tls = EMail::Client::TLSMode::STARTTLS

View file

@ -1,5 +1,5 @@
Hey, <%= user.name %>!
Hey, <%= user.firstname %>!
Du wurdest erfolgreich als Lehrer registriert.
Initialisiere deinen Account, indem du auf den folgenden Link klickst und deine Daten eingibst:
<%= Backend.config.url %>
<%= Path[Backend.config.url, "login"] %>

View file

@ -1,6 +1,6 @@
module Backend
SERVICES = [
API::SERVICE,
Api::SERVICE,
Worker::SERVICE,
]
end