407 lines
7.6 KiB
Crystal
407 lines
7.6 KiB
Crystal
|
require "kemal"
|
|||
|
require "uuid"
|
|||
|
require "file_utils"
|
|||
|
|
|||
|
require "authd"
|
|||
|
|
|||
|
class Task
|
|||
|
JSON.mapping({
|
|||
|
id: String,
|
|||
|
author: Int32,
|
|||
|
title: String,
|
|||
|
description: String,
|
|||
|
column: String,
|
|||
|
assigned_to: Int32?
|
|||
|
})
|
|||
|
|
|||
|
def initialize(@title, @author, @description, @column)
|
|||
|
@id = UUID.random.to_s
|
|||
|
end
|
|||
|
end
|
|||
|
|
|||
|
class Column
|
|||
|
JSON.mapping({
|
|||
|
id: String,
|
|||
|
name: String
|
|||
|
})
|
|||
|
|
|||
|
def initialize(@name)
|
|||
|
@id = UUID.random.to_s
|
|||
|
end
|
|||
|
end
|
|||
|
|
|||
|
class Project
|
|||
|
JSON.mapping({
|
|||
|
id: String,
|
|||
|
name: String,
|
|||
|
columns: Array(Column),
|
|||
|
tasks: Array(Task),
|
|||
|
color: {
|
|||
|
type: String,
|
|||
|
default: "dark"
|
|||
|
}
|
|||
|
})
|
|||
|
|
|||
|
def initialize(@name)
|
|||
|
@id = UUID.random.to_s
|
|||
|
|
|||
|
@columns = [Column.new "To do"]
|
|||
|
@tasks = [] of Task
|
|||
|
|
|||
|
@color = "dark"
|
|||
|
|
|||
|
write!
|
|||
|
end
|
|||
|
|
|||
|
def write!
|
|||
|
Dir.mkdir_p "./projects/#{@id}"
|
|||
|
|
|||
|
File.write "./projects/#{@id}/project.json.new", to_json
|
|||
|
FileUtils.mv "./projects/#{@id}/project.json.new",
|
|||
|
"./projects/#{@id}/project.json"
|
|||
|
end
|
|||
|
|
|||
|
# FIXME: Transform that exception somehow.
|
|||
|
# FIXME: Update to not read everything.
|
|||
|
def self.get_from_id(id)
|
|||
|
self.all.find(&.id.==(id)).not_nil!
|
|||
|
end
|
|||
|
|
|||
|
def self.all : Array(Project)
|
|||
|
# FIXME: switch to an index file per project in a storage directory.
|
|||
|
Dir.children("./projects").compact_map do |filename|
|
|||
|
begin
|
|||
|
Project.from_json File.read "./projects/#{filename}/project.json"
|
|||
|
rescue
|
|||
|
end
|
|||
|
end
|
|||
|
rescue
|
|||
|
[] of Project
|
|||
|
end
|
|||
|
end
|
|||
|
|
|||
|
class Requests::Login
|
|||
|
JSON.mapping({
|
|||
|
type: String,
|
|||
|
login: String,
|
|||
|
password: String
|
|||
|
})
|
|||
|
end
|
|||
|
|
|||
|
class Requests::Tasks
|
|||
|
JSON.mapping({
|
|||
|
type: String,
|
|||
|
project: String
|
|||
|
})
|
|||
|
end
|
|||
|
|
|||
|
class Requests::NewProject
|
|||
|
JSON.mapping({
|
|||
|
type: String,
|
|||
|
name: String
|
|||
|
})
|
|||
|
end
|
|||
|
|
|||
|
class Requests::EditProject
|
|||
|
JSON.mapping({
|
|||
|
type: String,
|
|||
|
project: String,
|
|||
|
name: String?,
|
|||
|
color: String?
|
|||
|
})
|
|||
|
end
|
|||
|
|
|||
|
class Requests::NewColumn
|
|||
|
JSON.mapping({
|
|||
|
type: String,
|
|||
|
project: String,
|
|||
|
name: String
|
|||
|
})
|
|||
|
end
|
|||
|
|
|||
|
class Requests::NewTask
|
|||
|
JSON.mapping({
|
|||
|
type: String,
|
|||
|
project: String,
|
|||
|
column: String,
|
|||
|
title: String,
|
|||
|
description: String
|
|||
|
})
|
|||
|
end
|
|||
|
|
|||
|
class Requests::EditTask
|
|||
|
JSON.mapping({
|
|||
|
type: String,
|
|||
|
project: String,
|
|||
|
task: String,
|
|||
|
column: String?,
|
|||
|
title: String?,
|
|||
|
description: String?,
|
|||
|
assigned_to: Int32?
|
|||
|
})
|
|||
|
end
|
|||
|
|
|||
|
class Requests::DeleteTask
|
|||
|
JSON.mapping({
|
|||
|
type: String,
|
|||
|
project: String,
|
|||
|
task: String
|
|||
|
})
|
|||
|
end
|
|||
|
|
|||
|
class Requests::EditColumn
|
|||
|
JSON.mapping({
|
|||
|
type: String,
|
|||
|
project: String,
|
|||
|
column: String,
|
|||
|
name: String?
|
|||
|
})
|
|||
|
end
|
|||
|
|
|||
|
class Requests::DeleteColumn
|
|||
|
JSON.mapping({
|
|||
|
type: String,
|
|||
|
project: String,
|
|||
|
column: String
|
|||
|
})
|
|||
|
end
|
|||
|
|
|||
|
class Requests::GetUser
|
|||
|
JSON.mapping({
|
|||
|
type: String,
|
|||
|
uid: Int32
|
|||
|
})
|
|||
|
end
|
|||
|
|
|||
|
authd = AuthD::Client.new
|
|||
|
|
|||
|
Kemal.config.extra_options do |parser|
|
|||
|
parser.on "-k file", "--jwt-key file", "Provides the JWT key for authd." do |file|
|
|||
|
authd.key = File.read(file).chomp
|
|||
|
end
|
|||
|
end
|
|||
|
|
|||
|
sockets = [] of HTTP::WebSocket
|
|||
|
users = {} of HTTP::WebSocket => AuthD::User
|
|||
|
|
|||
|
ws "/socket" do |socket|
|
|||
|
socket.on_close do
|
|||
|
users.delete socket
|
|||
|
sockets.delete socket
|
|||
|
end
|
|||
|
|
|||
|
socket.on_message do |message_as_s|
|
|||
|
message = JSON.parse(message_as_s).as_h
|
|||
|
|
|||
|
case message["type"]
|
|||
|
when "login"
|
|||
|
request = Requests::Login.from_json message_as_s
|
|||
|
|
|||
|
login = request.login
|
|||
|
password = request.password
|
|||
|
|
|||
|
user = authd.get_user? login, password
|
|||
|
|
|||
|
unless user
|
|||
|
socket.send({
|
|||
|
type: "login-error",
|
|||
|
error: "Invalid credentials."
|
|||
|
}.to_json)
|
|||
|
|
|||
|
next
|
|||
|
end
|
|||
|
|
|||
|
users[socket] = user
|
|||
|
sockets << socket
|
|||
|
|
|||
|
socket.send({
|
|||
|
type: "login"
|
|||
|
}.to_json)
|
|||
|
|
|||
|
socket.send({
|
|||
|
type: "list-projects",
|
|||
|
projects: Project.all
|
|||
|
}.to_json)
|
|||
|
when "new-project"
|
|||
|
user = users[socket] # FIXME: make it an authentication error
|
|||
|
|
|||
|
request = Requests::NewProject.from_json message_as_s
|
|||
|
|
|||
|
project = Project.new request.name
|
|||
|
|
|||
|
# FIXME: Only notify concerned users.
|
|||
|
sockets.each &.send({
|
|||
|
type: "project",
|
|||
|
project: project
|
|||
|
}.to_json)
|
|||
|
when "project"
|
|||
|
user = users[socket] # FIXME: make it an authentication error
|
|||
|
|
|||
|
request = Requests::Tasks.from_json message_as_s
|
|||
|
|
|||
|
project = Project.get_from_id request.project
|
|||
|
|
|||
|
socket.send({
|
|||
|
type: "project",
|
|||
|
project: project
|
|||
|
}.to_json)
|
|||
|
when "edit-project"
|
|||
|
user = users[socket] # FIXME: make it an authentication error
|
|||
|
|
|||
|
request = Requests::EditProject.from_json message_as_s
|
|||
|
|
|||
|
project = Project.get_from_id request.project
|
|||
|
|
|||
|
if name = request.name
|
|||
|
project.name = name
|
|||
|
end
|
|||
|
|
|||
|
if color = request.color
|
|||
|
project.color = color
|
|||
|
end
|
|||
|
|
|||
|
project.write!
|
|||
|
|
|||
|
# FIXME: Only notify concerned users.
|
|||
|
sockets.each &.send({
|
|||
|
type: "project",
|
|||
|
project: project
|
|||
|
}.to_json)
|
|||
|
when "new-column"
|
|||
|
user = users[socket] # FIXME: make it an authentication error
|
|||
|
|
|||
|
request = Requests::NewColumn.from_json message_as_s
|
|||
|
|
|||
|
project = Project.get_from_id request.project
|
|||
|
|
|||
|
project.columns << Column.new request.name
|
|||
|
project.write!
|
|||
|
|
|||
|
# FIXME: Only notify concerned users.
|
|||
|
sockets.each &.send({
|
|||
|
type: "project",
|
|||
|
project: project
|
|||
|
}.to_json)
|
|||
|
when "new-task"
|
|||
|
user = users[socket] # FIXME: make it an authentication error
|
|||
|
|
|||
|
request = Requests::NewTask.from_json message_as_s
|
|||
|
|
|||
|
project = Project.get_from_id request.project
|
|||
|
column = project.columns.find(&.id.==(request.column)).not_nil!
|
|||
|
|
|||
|
project.tasks << Task.new request.title, user.uid, request.description, column.id
|
|||
|
|
|||
|
project.write!
|
|||
|
|
|||
|
# FIXME: Only notify concerned users.
|
|||
|
sockets.each &.send({
|
|||
|
type: "project",
|
|||
|
project: project
|
|||
|
}.to_json)
|
|||
|
when "edit-task"
|
|||
|
user = users[socket] # FIXME: make it an authentication error
|
|||
|
|
|||
|
request = Requests::EditTask.from_json message_as_s
|
|||
|
|
|||
|
project = Project.get_from_id request.project
|
|||
|
task = project.tasks.find(&.id.==(request.task)).not_nil!
|
|||
|
|
|||
|
if column = request.column
|
|||
|
column = project.columns.find(&.id.==(column)).not_nil!
|
|||
|
|
|||
|
task.column = column.id
|
|||
|
end
|
|||
|
|
|||
|
if title = request.title
|
|||
|
task.title = title
|
|||
|
end
|
|||
|
|
|||
|
if description = request.description
|
|||
|
task.description = description
|
|||
|
end
|
|||
|
|
|||
|
# FIXME: Check it’s a valid UID.
|
|||
|
if assigned_to = request.assigned_to
|
|||
|
# FIXME: Probably not the best way to handle this corner-case.
|
|||
|
assigned_to = nil if assigned_to == -1
|
|||
|
|
|||
|
task.assigned_to = assigned_to
|
|||
|
end
|
|||
|
|
|||
|
project.write!
|
|||
|
|
|||
|
# FIXME: Only notify concerned users.
|
|||
|
sockets.each &.send({
|
|||
|
type: "project",
|
|||
|
project: project
|
|||
|
}.to_json)
|
|||
|
when "delete-task"
|
|||
|
user = users[socket]
|
|||
|
|
|||
|
request = Requests::DeleteTask.from_json message_as_s
|
|||
|
|
|||
|
project = Project.get_from_id request.project
|
|||
|
|
|||
|
project.tasks.select! &.id.!=(request.task)
|
|||
|
|
|||
|
project.write!
|
|||
|
|
|||
|
# FIXME: Only notify concerned users.
|
|||
|
sockets.each &.send({
|
|||
|
type: "project",
|
|||
|
project: project
|
|||
|
}.to_json)
|
|||
|
when "edit-column"
|
|||
|
user = users[socket] # FIXME: make it an authentication error
|
|||
|
|
|||
|
request = Requests::EditColumn.from_json message_as_s
|
|||
|
|
|||
|
project = Project.get_from_id request.project
|
|||
|
column = project.columns.find(&.id.==(request.column)).not_nil!
|
|||
|
|
|||
|
if name = request.name
|
|||
|
column.name = name
|
|||
|
end
|
|||
|
|
|||
|
puts project.columns.to_json
|
|||
|
|
|||
|
project.write!
|
|||
|
|
|||
|
# FIXME: Only notify concerned users.
|
|||
|
sockets.each &.send({
|
|||
|
type: "project",
|
|||
|
project: project
|
|||
|
}.to_json)
|
|||
|
when "delete-column"
|
|||
|
user = users[socket]
|
|||
|
|
|||
|
request = Requests::DeleteColumn.from_json message_as_s
|
|||
|
project = Project.get_from_id request.project
|
|||
|
|
|||
|
project.columns.select! &.id.!=(request.column)
|
|||
|
project.tasks.select! &.column.!=(request.column)
|
|||
|
|
|||
|
project.write!
|
|||
|
|
|||
|
# FIXME: Only notify concerned users.
|
|||
|
sockets.each &.send({
|
|||
|
type: "project",
|
|||
|
project: project
|
|||
|
}.to_json)
|
|||
|
when "get-user"
|
|||
|
request = Requests::GetUser.from_json message_as_s
|
|||
|
|
|||
|
user = authd.get_user? request.uid
|
|||
|
|
|||
|
socket.send({
|
|||
|
type: "user",
|
|||
|
user: user
|
|||
|
}.to_json)
|
|||
|
end
|
|||
|
end
|
|||
|
end
|
|||
|
|
|||
|
Kemal.run
|
|||
|
|