feat: expose CRUD, onboarding, pubsub via web
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
defmodule Localiser.Web.Controllers.FloorController do
|
||||
use Phoenix.Controller, formats: [:json]
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
alias Localiser.Domain.Floors
|
||||
alias Localiser.Web.Schemas
|
||||
|
||||
tags ["Floors"]
|
||||
security [%{"bearerAuth" => []}]
|
||||
|
||||
operation :index,
|
||||
summary: "List all floors",
|
||||
responses: [
|
||||
ok: {"Floor list", "application/json", %OpenApiSpex.Schema{type: :array, items: Schemas.Floor}},
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
|
||||
]
|
||||
|
||||
operation :show,
|
||||
summary: "Get a floor",
|
||||
parameters: [id: [in: :path, type: :integer, required: true]],
|
||||
responses: [
|
||||
ok: {"Floor", "application/json", Schemas.Floor},
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
|
||||
]
|
||||
|
||||
operation :create,
|
||||
summary: "Create a floor",
|
||||
request_body: {"Floor params", "application/json", Schemas.FloorParams, required: true},
|
||||
responses: [
|
||||
created: {"Created floor", "application/json", Schemas.Floor},
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error},
|
||||
unprocessable_entity: {"Validation errors", "application/json", Schemas.ValidationErrors}
|
||||
]
|
||||
|
||||
operation :update,
|
||||
summary: "Update a floor",
|
||||
parameters: [id: [in: :path, type: :integer, required: true]],
|
||||
request_body: {"Floor params", "application/json", Schemas.FloorUpdateParams},
|
||||
responses: [
|
||||
ok: {"Updated floor", "application/json", Schemas.Floor},
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error},
|
||||
unprocessable_entity: {"Validation errors", "application/json", Schemas.ValidationErrors}
|
||||
]
|
||||
|
||||
operation :delete,
|
||||
summary: "Delete a floor (also deletes all rooms and unenrols sensors)",
|
||||
parameters: [id: [in: :path, type: :integer, required: true]],
|
||||
responses: [
|
||||
no_content: "Deleted",
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
|
||||
]
|
||||
|
||||
def index(conn, _params) do
|
||||
json(conn, Enum.map(Floors.list_floors(), &render_floor/1))
|
||||
end
|
||||
|
||||
def show(conn, %{"id" => id}) do
|
||||
floor = Floors.get_floor!(id)
|
||||
json(conn, render_floor(floor))
|
||||
end
|
||||
|
||||
def create(conn, params) do
|
||||
case Floors.create_floor(params) do
|
||||
{:ok, floor} ->
|
||||
conn
|
||||
|> put_status(:created)
|
||||
|> json(render_floor(floor))
|
||||
|
||||
{:error, changeset} ->
|
||||
conn
|
||||
|> put_status(:unprocessable_entity)
|
||||
|> json(%{errors: format_errors(changeset)})
|
||||
end
|
||||
end
|
||||
|
||||
def update(conn, %{"id" => id} = params) do
|
||||
floor = Floors.get_floor!(id)
|
||||
attrs = Map.drop(params, ["id"])
|
||||
|
||||
case Floors.update_floor(floor, attrs) do
|
||||
{:ok, updated} ->
|
||||
json(conn, render_floor(updated))
|
||||
|
||||
{:error, changeset} ->
|
||||
conn
|
||||
|> put_status(:unprocessable_entity)
|
||||
|> json(%{errors: format_errors(changeset)})
|
||||
end
|
||||
end
|
||||
|
||||
def delete(conn, %{"id" => id}) do
|
||||
floor = Floors.get_floor!(id)
|
||||
{:ok, _} = Floors.delete_floor(floor)
|
||||
send_resp(conn, :no_content, "")
|
||||
end
|
||||
|
||||
defp render_floor(floor) do
|
||||
%{
|
||||
id: floor.id,
|
||||
name: floor.name,
|
||||
width: floor.width,
|
||||
height: floor.height
|
||||
}
|
||||
end
|
||||
|
||||
defp format_errors(changeset) do
|
||||
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
|
||||
Enum.reduce(opts, msg, fn {key, val}, acc ->
|
||||
String.replace(acc, "%{#{key}}", to_string(val))
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,20 @@
|
||||
defmodule Localiser.Web.Controllers.OnboardingController do
|
||||
use Phoenix.Controller, formats: [:json]
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
alias Localiser.Domain.System
|
||||
alias Localiser.Web.Schemas
|
||||
|
||||
tags ["Onboarding"]
|
||||
security []
|
||||
|
||||
operation :status,
|
||||
summary: "Get onboarding checklist status",
|
||||
responses: [
|
||||
ok: {"Onboarding status", "application/json", Schemas.OnboardingStatus}
|
||||
]
|
||||
|
||||
def status(conn, _params) do
|
||||
json(conn, System.onboarding_status())
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,130 @@
|
||||
defmodule Localiser.Web.Controllers.RoomController do
|
||||
use Phoenix.Controller, formats: [:json]
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
alias Localiser.Domain.Rooms
|
||||
alias Localiser.Web.Schemas
|
||||
|
||||
tags ["Rooms"]
|
||||
security [%{"bearerAuth" => []}]
|
||||
|
||||
operation :index,
|
||||
summary: "List rooms for a floor",
|
||||
parameters: [floor_id: [in: :path, type: :integer, required: true]],
|
||||
responses: [
|
||||
ok: {"Room list", "application/json", %OpenApiSpex.Schema{type: :array, items: Schemas.Room}},
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
|
||||
]
|
||||
|
||||
operation :show,
|
||||
summary: "Get a room",
|
||||
parameters: [
|
||||
floor_id: [in: :path, type: :integer, required: true],
|
||||
id: [in: :path, type: :integer, required: true]
|
||||
],
|
||||
responses: [
|
||||
ok: {"Room", "application/json", Schemas.Room},
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
|
||||
]
|
||||
|
||||
operation :create,
|
||||
summary: "Create a room",
|
||||
parameters: [floor_id: [in: :path, type: :integer, required: true]],
|
||||
request_body: {"Room params", "application/json", Schemas.RoomParams, required: true},
|
||||
responses: [
|
||||
created: {"Created room", "application/json", Schemas.Room},
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error},
|
||||
unprocessable_entity: {"Validation errors", "application/json", Schemas.ValidationErrors}
|
||||
]
|
||||
|
||||
operation :update,
|
||||
summary: "Update a room",
|
||||
parameters: [
|
||||
floor_id: [in: :path, type: :integer, required: true],
|
||||
id: [in: :path, type: :integer, required: true]
|
||||
],
|
||||
request_body: {"Room params", "application/json", Schemas.RoomUpdateParams},
|
||||
responses: [
|
||||
ok: {"Updated room", "application/json", Schemas.Room},
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error},
|
||||
unprocessable_entity: {"Validation errors", "application/json", Schemas.ValidationErrors}
|
||||
]
|
||||
|
||||
operation :delete,
|
||||
summary: "Delete a room (also unenrols sensors)",
|
||||
parameters: [
|
||||
floor_id: [in: :path, type: :integer, required: true],
|
||||
id: [in: :path, type: :integer, required: true]
|
||||
],
|
||||
responses: [
|
||||
no_content: "Deleted",
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
|
||||
]
|
||||
|
||||
def index(conn, %{"floor_id" => floor_id}) do
|
||||
rooms = Rooms.list_rooms_for_floor(floor_id)
|
||||
json(conn, Enum.map(rooms, &render_room/1))
|
||||
end
|
||||
|
||||
def show(conn, %{"id" => id}) do
|
||||
room = Rooms.get_room!(id)
|
||||
json(conn, render_room(room))
|
||||
end
|
||||
|
||||
def create(conn, %{"floor_id" => floor_id} = params) do
|
||||
attrs = Map.put(params, "floor_id", floor_id)
|
||||
|
||||
case Rooms.create_room(attrs) do
|
||||
{:ok, room} ->
|
||||
conn
|
||||
|> put_status(:created)
|
||||
|> json(render_room(room))
|
||||
|
||||
{:error, changeset} ->
|
||||
conn
|
||||
|> put_status(:unprocessable_entity)
|
||||
|> json(%{errors: format_errors(changeset)})
|
||||
end
|
||||
end
|
||||
|
||||
def update(conn, %{"id" => id} = params) do
|
||||
room = Rooms.get_room!(id)
|
||||
attrs = Map.drop(params, ["id", "floor_id"])
|
||||
|
||||
case Rooms.update_room(room, attrs) do
|
||||
{:ok, updated} ->
|
||||
json(conn, render_room(updated))
|
||||
|
||||
{:error, changeset} ->
|
||||
conn
|
||||
|> put_status(:unprocessable_entity)
|
||||
|> json(%{errors: format_errors(changeset)})
|
||||
end
|
||||
end
|
||||
|
||||
def delete(conn, %{"id" => id}) do
|
||||
room = Rooms.get_room!(id)
|
||||
{:ok, _} = Rooms.delete_room(room)
|
||||
send_resp(conn, :no_content, "")
|
||||
end
|
||||
|
||||
defp render_room(room) do
|
||||
%{
|
||||
id: room.id,
|
||||
name: room.name,
|
||||
floor_id: room.floor_id,
|
||||
x: room.x,
|
||||
y: room.y,
|
||||
width: room.width,
|
||||
height: room.height
|
||||
}
|
||||
end
|
||||
|
||||
defp format_errors(changeset) do
|
||||
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
|
||||
Enum.reduce(opts, msg, fn {key, val}, acc ->
|
||||
String.replace(acc, "%{#{key}}", to_string(val))
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,193 @@
|
||||
defmodule Localiser.Web.Controllers.SensorController do
|
||||
use Phoenix.Controller, formats: [:json]
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
alias Localiser.Domain.Sensors
|
||||
alias Localiser.Localisation.Sensor.Server, as: SensorServer
|
||||
alias Localiser.Web.Schemas
|
||||
|
||||
tags ["Sensors"]
|
||||
security [%{"bearerAuth" => []}]
|
||||
|
||||
operation :index,
|
||||
summary: "List all sensors",
|
||||
responses: [
|
||||
ok: {"Sensor list", "application/json", %OpenApiSpex.Schema{type: :array, items: Schemas.Sensor}},
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
|
||||
]
|
||||
|
||||
operation :unplaced,
|
||||
summary: "List sensors not yet assigned to a room",
|
||||
responses: [
|
||||
ok: {"Sensor list", "application/json", %OpenApiSpex.Schema{type: :array, items: Schemas.Sensor}},
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
|
||||
]
|
||||
|
||||
operation :show,
|
||||
summary: "Get a sensor",
|
||||
parameters: [id: [in: :path, type: :integer, required: true]],
|
||||
responses: [
|
||||
ok: {"Sensor", "application/json", Schemas.Sensor},
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
|
||||
]
|
||||
|
||||
operation :update,
|
||||
summary: "Update sensor metadata",
|
||||
parameters: [id: [in: :path, type: :integer, required: true]],
|
||||
request_body: {"Sensor params", "application/json", Schemas.SensorUpdateParams},
|
||||
responses: [
|
||||
ok: {"Updated sensor", "application/json", Schemas.Sensor},
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error},
|
||||
unprocessable_entity: {"Validation errors", "application/json", Schemas.ValidationErrors}
|
||||
]
|
||||
|
||||
operation :delete,
|
||||
summary: "Delete / unenrol a sensor",
|
||||
parameters: [id: [in: :path, type: :integer, required: true]],
|
||||
responses: [
|
||||
no_content: "Deleted",
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
|
||||
]
|
||||
|
||||
operation :place,
|
||||
summary: "Place sensor at a position in a room",
|
||||
parameters: [id: [in: :path, type: :integer, required: true]],
|
||||
request_body: {"Place params", "application/json", Schemas.SensorPlaceParams, required: true},
|
||||
responses: [
|
||||
ok: {"Updated sensor", "application/json", Schemas.Sensor},
|
||||
bad_request: {"Missing params", "application/json", Schemas.Error},
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error},
|
||||
unprocessable_entity: {"Validation errors", "application/json", Schemas.ValidationErrors}
|
||||
]
|
||||
|
||||
operation :unplace,
|
||||
summary: "Remove sensor from floor layout",
|
||||
parameters: [id: [in: :path, type: :integer, required: true]],
|
||||
responses: [
|
||||
ok: {"Updated sensor", "application/json", Schemas.Sensor},
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
|
||||
]
|
||||
|
||||
operation :calibration_start,
|
||||
summary: "Begin RSSI calibration",
|
||||
parameters: [id: [in: :path, type: :integer, required: true]],
|
||||
request_body: {"Calibration params", "application/json", Schemas.CalibrationStartParams, required: true},
|
||||
responses: [
|
||||
ok: {"Calibration started", "application/json", Schemas.CalibrationStatus},
|
||||
bad_request: {"Missing reference_distance", "application/json", Schemas.Error},
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
|
||||
]
|
||||
|
||||
operation :calibration_stop,
|
||||
summary: "Abort active calibration",
|
||||
parameters: [id: [in: :path, type: :integer, required: true]],
|
||||
responses: [
|
||||
ok: {"Calibration aborted", "application/json", Schemas.CalibrationStatus},
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
|
||||
]
|
||||
|
||||
def index(conn, _params) do
|
||||
json(conn, Enum.map(Sensors.list_sensors(), &render_sensor/1))
|
||||
end
|
||||
|
||||
def unplaced(conn, _params) do
|
||||
json(conn, Enum.map(Sensors.list_unplaced(), &render_sensor/1))
|
||||
end
|
||||
|
||||
def show(conn, %{"id" => id}) do
|
||||
sensor = Sensors.get_sensor!(id)
|
||||
json(conn, render_sensor(sensor))
|
||||
end
|
||||
|
||||
def update(conn, %{"id" => id} = params) do
|
||||
sensor = Sensors.get_sensor!(id)
|
||||
attrs = Map.drop(params, ["id"])
|
||||
|
||||
case Sensors.update_sensor(sensor, attrs) do
|
||||
{:ok, updated} ->
|
||||
json(conn, render_sensor(updated))
|
||||
|
||||
{:error, changeset} ->
|
||||
conn
|
||||
|> put_status(:unprocessable_entity)
|
||||
|> json(%{errors: format_errors(changeset)})
|
||||
end
|
||||
end
|
||||
|
||||
def delete(conn, %{"id" => id}) do
|
||||
sensor = Sensors.get_sensor!(id)
|
||||
{:ok, _} = Sensors.delete_sensor(sensor)
|
||||
send_resp(conn, :no_content, "")
|
||||
end
|
||||
|
||||
def place(conn, %{"id" => id, "room_id" => room_id, "x" => x, "y" => y}) do
|
||||
sensor = Sensors.get_sensor!(id)
|
||||
|
||||
case Sensors.place_sensor(sensor, room_id, {x, y}) do
|
||||
{:ok, updated} ->
|
||||
json(conn, render_sensor(updated))
|
||||
|
||||
{:error, changeset} ->
|
||||
conn
|
||||
|> put_status(:unprocessable_entity)
|
||||
|> json(%{errors: format_errors(changeset)})
|
||||
end
|
||||
end
|
||||
|
||||
def place(conn, _params) do
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "room_id, x, and y are required"})
|
||||
end
|
||||
|
||||
def unplace(conn, %{"id" => id}) do
|
||||
sensor = Sensors.get_sensor!(id)
|
||||
|
||||
case Sensors.remove_from_layout(sensor) do
|
||||
{:ok, updated} ->
|
||||
json(conn, render_sensor(updated))
|
||||
|
||||
{:error, changeset} ->
|
||||
conn
|
||||
|> put_status(:unprocessable_entity)
|
||||
|> json(%{errors: format_errors(changeset)})
|
||||
end
|
||||
end
|
||||
|
||||
def calibration_start(conn, %{"id" => id, "reference_distance" => ref_dist}) do
|
||||
sensor = Sensors.get_sensor!(id)
|
||||
:ok = SensorServer.begin_calibration(sensor.sensor_id, ref_dist)
|
||||
json(conn, %{status: "calibrating"})
|
||||
end
|
||||
|
||||
def calibration_start(conn, _params) do
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "reference_distance is required"})
|
||||
end
|
||||
|
||||
def calibration_stop(conn, %{"id" => id}) do
|
||||
sensor = Sensors.get_sensor!(id)
|
||||
:ok = SensorServer.abort_calibration(sensor.sensor_id)
|
||||
json(conn, %{status: "idle"})
|
||||
end
|
||||
|
||||
defp render_sensor(sensor) do
|
||||
%{
|
||||
id: sensor.id,
|
||||
sensor_id: sensor.sensor_id,
|
||||
room_id: sensor.room_id,
|
||||
floor_x: sensor.floor_x,
|
||||
floor_y: sensor.floor_y,
|
||||
rssi_ref: sensor.rssi_ref
|
||||
}
|
||||
end
|
||||
|
||||
defp format_errors(changeset) do
|
||||
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
|
||||
Enum.reduce(opts, msg, fn {key, val}, acc ->
|
||||
String.replace(acc, "%{#{key}}", to_string(val))
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,52 @@
|
||||
defmodule Localiser.Web.Controllers.SessionController do
|
||||
use Phoenix.Controller, formats: [:json]
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
alias Localiser.Domain.Users
|
||||
alias Localiser.Web.Token
|
||||
alias Localiser.Web.Schemas
|
||||
|
||||
tags ["Auth"]
|
||||
security []
|
||||
|
||||
operation :create,
|
||||
summary: "Log in and receive a JWT",
|
||||
request_body: {"Login params", "application/json", Schemas.SessionParams, required: true},
|
||||
responses: [
|
||||
ok: {"JWT + user", "application/json", Schemas.TokenResponse},
|
||||
bad_request: {"Missing username/password", "application/json", Schemas.Error},
|
||||
unauthorized: {"Invalid credentials", "application/json", Schemas.Error}
|
||||
]
|
||||
|
||||
operation :delete,
|
||||
summary: "Log out (client discards token)",
|
||||
responses: [no_content: "Logged out"]
|
||||
|
||||
def create(conn, %{"username" => username, "password" => password}) do
|
||||
case Users.authenticate_user(username, password) do
|
||||
{:ok, user} ->
|
||||
token = Token.generate(%{"sub" => user.id})
|
||||
json(conn, %{token: token, user: render_user(user)})
|
||||
|
||||
{:error, :invalid_credentials} ->
|
||||
conn
|
||||
|> put_status(:unauthorized)
|
||||
|> json(%{error: "invalid credentials"})
|
||||
end
|
||||
end
|
||||
|
||||
def create(conn, _params) do
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "username and password are required"})
|
||||
end
|
||||
|
||||
def delete(conn, _params) do
|
||||
# JWT is stateless; client discards token
|
||||
send_resp(conn, :no_content, "")
|
||||
end
|
||||
|
||||
defp render_user(user) do
|
||||
%{id: user.id, username: user.username, is_admin: user.is_admin}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,57 @@
|
||||
defmodule Localiser.Web.Controllers.SetupController do
|
||||
use Phoenix.Controller, formats: [:json]
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
alias Localiser.Domain.Users
|
||||
alias Localiser.Web.Token
|
||||
alias Localiser.Web.Schemas
|
||||
|
||||
tags ["Auth"]
|
||||
|
||||
operation :create,
|
||||
summary: "Create first admin user",
|
||||
description: "Only succeeds when no users exist. Blocked with 403 afterwards.",
|
||||
security: [],
|
||||
request_body: {"Setup params", "application/json", Schemas.SetupParams, required: true},
|
||||
responses: [
|
||||
created: {"Admin user created", "application/json", Schemas.TokenResponse},
|
||||
bad_request: {"Missing username/password", "application/json", Schemas.Error},
|
||||
forbidden: {"System already initialised", "application/json", Schemas.Error},
|
||||
unprocessable_entity: {"Validation errors", "application/json", Schemas.ValidationErrors}
|
||||
]
|
||||
|
||||
def create(conn, %{"username" => username, "password" => password}) do
|
||||
attrs = %{"username" => username, "password" => password, "is_admin" => true}
|
||||
|
||||
case Users.create_user(attrs) do
|
||||
{:ok, user} ->
|
||||
token = Token.generate(%{"sub" => user.id})
|
||||
conn
|
||||
|> put_status(:created)
|
||||
|> json(%{token: token, user: render_user(user)})
|
||||
|
||||
{:error, changeset} ->
|
||||
conn
|
||||
|> put_status(:unprocessable_entity)
|
||||
|> json(%{errors: format_errors(changeset)})
|
||||
end
|
||||
end
|
||||
|
||||
def create(conn, _params) do
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "username and password are required"})
|
||||
end
|
||||
|
||||
defp render_user(user) do
|
||||
%{id: user.id, username: user.username, is_admin: user.is_admin}
|
||||
end
|
||||
|
||||
defp format_errors(changeset) do
|
||||
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
|
||||
Enum.reduce(opts, msg, fn {key, val}, acc ->
|
||||
String.replace(acc, "%{#{key}}", to_string(val))
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,112 @@
|
||||
defmodule Localiser.Web.Controllers.TagController do
|
||||
use Phoenix.Controller, formats: [:json]
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
alias Localiser.Domain.Tags
|
||||
alias Localiser.Web.Schemas
|
||||
|
||||
tags ["Tags"]
|
||||
security [%{"bearerAuth" => []}]
|
||||
|
||||
operation :index,
|
||||
summary: "List all tags",
|
||||
responses: [
|
||||
ok: {"Tag list", "application/json", %OpenApiSpex.Schema{type: :array, items: Schemas.Tag}},
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
|
||||
]
|
||||
|
||||
operation :show,
|
||||
summary: "Get a tag",
|
||||
parameters: [id: [in: :path, type: :integer, required: true]],
|
||||
responses: [
|
||||
ok: {"Tag", "application/json", Schemas.Tag},
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
|
||||
]
|
||||
|
||||
operation :create,
|
||||
summary: "Enroll a tag",
|
||||
request_body: {"Tag params", "application/json", Schemas.TagParams, required: true},
|
||||
responses: [
|
||||
created: {"Enrolled tag", "application/json", Schemas.Tag},
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error},
|
||||
unprocessable_entity: {"Validation errors", "application/json", Schemas.ValidationErrors}
|
||||
]
|
||||
|
||||
operation :update,
|
||||
summary: "Update a tag",
|
||||
parameters: [id: [in: :path, type: :integer, required: true]],
|
||||
request_body: {"Tag params", "application/json", Schemas.TagUpdateParams},
|
||||
responses: [
|
||||
ok: {"Updated tag", "application/json", Schemas.Tag},
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error},
|
||||
unprocessable_entity: {"Validation errors", "application/json", Schemas.ValidationErrors}
|
||||
]
|
||||
|
||||
operation :delete,
|
||||
summary: "Delete / unenroll a tag",
|
||||
parameters: [id: [in: :path, type: :integer, required: true]],
|
||||
responses: [
|
||||
no_content: "Deleted",
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
|
||||
]
|
||||
|
||||
def index(conn, _params) do
|
||||
json(conn, Enum.map(Tags.list_tags(), &render_tag/1))
|
||||
end
|
||||
|
||||
def show(conn, %{"id" => id}) do
|
||||
tag = Tags.get_tag!(id)
|
||||
json(conn, render_tag(tag))
|
||||
end
|
||||
|
||||
def create(conn, params) do
|
||||
case Tags.create_tag(params) do
|
||||
{:ok, tag} ->
|
||||
conn
|
||||
|> put_status(:created)
|
||||
|> json(render_tag(tag))
|
||||
|
||||
{:error, changeset} ->
|
||||
conn
|
||||
|> put_status(:unprocessable_entity)
|
||||
|> json(%{errors: format_errors(changeset)})
|
||||
end
|
||||
end
|
||||
|
||||
def update(conn, %{"id" => id} = params) do
|
||||
tag = Tags.get_tag!(id)
|
||||
attrs = Map.drop(params, ["id"])
|
||||
|
||||
case Tags.update_tag(tag, attrs) do
|
||||
{:ok, updated} ->
|
||||
json(conn, render_tag(updated))
|
||||
|
||||
{:error, changeset} ->
|
||||
conn
|
||||
|> put_status(:unprocessable_entity)
|
||||
|> json(%{errors: format_errors(changeset)})
|
||||
end
|
||||
end
|
||||
|
||||
def delete(conn, %{"id" => id}) do
|
||||
tag = Tags.get_tag!(id)
|
||||
{:ok, _} = Tags.delete_tag(tag)
|
||||
send_resp(conn, :no_content, "")
|
||||
end
|
||||
|
||||
defp render_tag(tag) do
|
||||
%{
|
||||
id: tag.id,
|
||||
tag_id: tag.tag_id,
|
||||
name: tag.name
|
||||
}
|
||||
end
|
||||
|
||||
defp format_errors(changeset) do
|
||||
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
|
||||
Enum.reduce(opts, msg, fn {key, val}, acc ->
|
||||
String.replace(acc, "%{#{key}}", to_string(val))
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,147 @@
|
||||
defmodule Localiser.Web.Controllers.UserController do
|
||||
use Phoenix.Controller, formats: [:json]
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
alias Localiser.Domain.Users
|
||||
alias Localiser.Web.Schemas
|
||||
|
||||
tags ["Users"]
|
||||
security [%{"bearerAuth" => []}]
|
||||
|
||||
operation :me,
|
||||
summary: "Get own profile",
|
||||
responses: [
|
||||
ok: {"Current user", "application/json", Schemas.User},
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error}
|
||||
]
|
||||
|
||||
operation :index,
|
||||
summary: "List all users (admin)",
|
||||
responses: [
|
||||
ok: {"User list", "application/json", %OpenApiSpex.Schema{type: :array, items: Schemas.User}},
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error},
|
||||
forbidden: {"Forbidden", "application/json", Schemas.Error}
|
||||
]
|
||||
|
||||
operation :show,
|
||||
summary: "Get a user (admin)",
|
||||
parameters: [id: [in: :path, type: :integer, required: true]],
|
||||
responses: [
|
||||
ok: {"User", "application/json", Schemas.User},
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error},
|
||||
forbidden: {"Forbidden", "application/json", Schemas.Error}
|
||||
]
|
||||
|
||||
operation :create,
|
||||
summary: "Create a user (admin)",
|
||||
request_body: {"User params", "application/json", Schemas.UserCreateParams, required: true},
|
||||
responses: [
|
||||
created: {"Created user", "application/json", Schemas.User},
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error},
|
||||
forbidden: {"Forbidden", "application/json", Schemas.Error},
|
||||
unprocessable_entity: {"Validation errors", "application/json", Schemas.ValidationErrors}
|
||||
]
|
||||
|
||||
operation :update,
|
||||
summary: "Update a user (admin)",
|
||||
parameters: [id: [in: :path, type: :integer, required: true]],
|
||||
request_body: {"User params", "application/json", Schemas.UserUpdateParams},
|
||||
responses: [
|
||||
ok: {"Updated user", "application/json", Schemas.User},
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error},
|
||||
forbidden: {"Forbidden", "application/json", Schemas.Error},
|
||||
unprocessable_entity: {"Validation errors", "application/json", Schemas.ValidationErrors}
|
||||
]
|
||||
|
||||
operation :delete,
|
||||
summary: "Delete a user (admin)",
|
||||
parameters: [id: [in: :path, type: :integer, required: true]],
|
||||
responses: [
|
||||
no_content: "Deleted",
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error},
|
||||
forbidden: {"Forbidden", "application/json", Schemas.Error}
|
||||
]
|
||||
|
||||
operation :promote,
|
||||
summary: "Promote a user to admin",
|
||||
parameters: [id: [in: :path, type: :integer, required: true]],
|
||||
responses: [
|
||||
ok: {"Updated user", "application/json", Schemas.User},
|
||||
unauthorized: {"Unauthorized", "application/json", Schemas.Error},
|
||||
forbidden: {"Forbidden", "application/json", Schemas.Error}
|
||||
]
|
||||
|
||||
def me(conn, _params) do
|
||||
json(conn, render_user(conn.assigns.current_user))
|
||||
end
|
||||
|
||||
def index(conn, _params) do
|
||||
json(conn, Enum.map(Users.list_users(), &render_user/1))
|
||||
end
|
||||
|
||||
def show(conn, %{"id" => id}) do
|
||||
user = Users.get_user!(id)
|
||||
json(conn, render_user(user))
|
||||
end
|
||||
|
||||
def create(conn, params) do
|
||||
case Users.create_user(params) do
|
||||
{:ok, user} ->
|
||||
conn
|
||||
|> put_status(:created)
|
||||
|> json(render_user(user))
|
||||
|
||||
{:error, changeset} ->
|
||||
conn
|
||||
|> put_status(:unprocessable_entity)
|
||||
|> json(%{errors: format_errors(changeset)})
|
||||
end
|
||||
end
|
||||
|
||||
def update(conn, %{"id" => id} = params) do
|
||||
user = Users.get_user!(id)
|
||||
attrs = Map.drop(params, ["id"])
|
||||
|
||||
case Users.update_user(user, attrs) do
|
||||
{:ok, updated} ->
|
||||
json(conn, render_user(updated))
|
||||
|
||||
{:error, changeset} ->
|
||||
conn
|
||||
|> put_status(:unprocessable_entity)
|
||||
|> json(%{errors: format_errors(changeset)})
|
||||
end
|
||||
end
|
||||
|
||||
def delete(conn, %{"id" => id}) do
|
||||
user = Users.get_user!(id)
|
||||
{:ok, _} = Users.delete_user(user)
|
||||
send_resp(conn, :no_content, "")
|
||||
end
|
||||
|
||||
def promote(conn, %{"id" => id}) do
|
||||
user = Users.get_user!(id)
|
||||
|
||||
case Users.promote_to_admin(user) do
|
||||
{:ok, updated} ->
|
||||
json(conn, render_user(updated))
|
||||
|
||||
{:error, changeset} ->
|
||||
conn
|
||||
|> put_status(:unprocessable_entity)
|
||||
|> json(%{errors: format_errors(changeset)})
|
||||
end
|
||||
end
|
||||
|
||||
defp render_user(user) do
|
||||
%{id: user.id, username: user.username, is_admin: user.is_admin}
|
||||
end
|
||||
|
||||
defp format_errors(changeset) do
|
||||
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
|
||||
Enum.reduce(opts, msg, fn {key, val}, acc ->
|
||||
String.replace(acc, "%{#{key}}", to_string(val))
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user