mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
feat(portal): Add client endpoints to REST API (#8355)
Adds the following endpoints: - `PUT /clients/:id` for updating the `name` - `PUT /clients/:client_id/verify` for verifying a client - `PUT /clients/:client_id/unverify` for unverifying a client - `GET /clients` for listing clients in an account - `GET /clients/:id` for getting a single client - `DELETE /clients/:id` for deleting a client Related: #8081
This commit is contained in:
167
elixir/apps/api/lib/api/controllers/client_controller.ex
Normal file
167
elixir/apps/api/lib/api/controllers/client_controller.ex
Normal file
@@ -0,0 +1,167 @@
|
||||
defmodule API.ClientController do
|
||||
use API, :controller
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
alias API.Pagination
|
||||
alias Domain.Clients
|
||||
|
||||
action_fallback(API.FallbackController)
|
||||
|
||||
tags(["Clients"])
|
||||
|
||||
operation(:index,
|
||||
summary: "List Clients",
|
||||
parameters: [
|
||||
limit: [
|
||||
in: :query,
|
||||
description: "Limit Clients returned",
|
||||
type: :integer,
|
||||
example: 10
|
||||
],
|
||||
page_cursor: [in: :query, description: "Next/Prev page cursor", type: :string]
|
||||
],
|
||||
responses: [
|
||||
ok: {"Client Response", "application/json", API.Schemas.Client.ListResponse}
|
||||
]
|
||||
)
|
||||
|
||||
# List Clients
|
||||
def index(conn, params) do
|
||||
list_opts =
|
||||
params
|
||||
|> Pagination.params_to_list_opts()
|
||||
|> Keyword.put(:preload, :online?)
|
||||
|
||||
with {:ok, clients, metadata} <- Clients.list_clients(conn.assigns.subject, list_opts) do
|
||||
render(conn, :index, clients: clients, metadata: metadata)
|
||||
end
|
||||
end
|
||||
|
||||
operation(:show,
|
||||
summary: "Show Client",
|
||||
parameters: [
|
||||
id: [
|
||||
in: :path,
|
||||
description: "Client ID",
|
||||
type: :string,
|
||||
example: "00000000-0000-0000-0000-000000000000"
|
||||
]
|
||||
],
|
||||
responses: [
|
||||
ok: {"Client Response", "application/json", API.Schemas.Client.Response}
|
||||
]
|
||||
)
|
||||
|
||||
# Show a specific Client
|
||||
def show(conn, %{"id" => id}) do
|
||||
with {:ok, client} <-
|
||||
Clients.fetch_client_by_id(id, conn.assigns.subject, preload: :online?) do
|
||||
render(conn, :show, client: client)
|
||||
end
|
||||
end
|
||||
|
||||
operation(:update,
|
||||
summary: "Update Client",
|
||||
parameters: [
|
||||
id: [
|
||||
in: :path,
|
||||
description: "Client ID",
|
||||
type: :string,
|
||||
example: "00000000-0000-0000-0000-000000000000"
|
||||
]
|
||||
],
|
||||
request_body:
|
||||
{"Client Attributes", "application/json", API.Schemas.Client.Request, required: true},
|
||||
responses: [
|
||||
ok: {"Client Response", "application/json", API.Schemas.Client.Response}
|
||||
]
|
||||
)
|
||||
|
||||
# Update a Client
|
||||
def update(conn, %{"id" => id, "client" => params}) do
|
||||
subject = conn.assigns.subject
|
||||
|
||||
with {:ok, client} <- Clients.fetch_client_by_id(id, subject, preload: :online?),
|
||||
{:ok, client} <- Clients.update_client(client, params, subject) do
|
||||
render(conn, :show, client: client)
|
||||
end
|
||||
end
|
||||
|
||||
def update(_conn, _params) do
|
||||
{:error, :bad_request}
|
||||
end
|
||||
|
||||
operation(:verify,
|
||||
summary: "Verify Client",
|
||||
parameters: [
|
||||
id: [
|
||||
in: :path,
|
||||
description: "Client ID",
|
||||
type: :string,
|
||||
example: "00000000-0000-0000-0000-000000000000"
|
||||
]
|
||||
],
|
||||
responses: [
|
||||
ok: {"Client Response", "application/json", API.Schemas.Client.Response}
|
||||
]
|
||||
)
|
||||
|
||||
# Verify a Client
|
||||
def verify(conn, %{"id" => id}) do
|
||||
subject = conn.assigns.subject
|
||||
|
||||
with {:ok, client} <- Clients.fetch_client_by_id(id, subject, preload: :online?),
|
||||
{:ok, client} <- Clients.verify_client(client, subject) do
|
||||
render(conn, :show, client: client)
|
||||
end
|
||||
end
|
||||
|
||||
operation(:unverify,
|
||||
summary: "Unverify Client",
|
||||
parameters: [
|
||||
id: [
|
||||
in: :path,
|
||||
description: "Client ID",
|
||||
type: :string,
|
||||
example: "00000000-0000-0000-0000-000000000000"
|
||||
]
|
||||
],
|
||||
responses: [
|
||||
ok: {"Client Response", "application/json", API.Schemas.Client.Response}
|
||||
]
|
||||
)
|
||||
|
||||
# Unverify a Client
|
||||
def unverify(conn, %{"id" => id}) do
|
||||
subject = conn.assigns.subject
|
||||
|
||||
with {:ok, client} <- Clients.fetch_client_by_id(id, subject, preload: :online?),
|
||||
{:ok, client} <- Clients.remove_client_verification(client, subject) do
|
||||
render(conn, :show, client: client)
|
||||
end
|
||||
end
|
||||
|
||||
operation(:delete,
|
||||
summary: "Delete a Client",
|
||||
parameters: [
|
||||
id: [
|
||||
in: :path,
|
||||
description: "Client ID",
|
||||
type: :string,
|
||||
example: "00000000-0000-0000-0000-000000000000"
|
||||
]
|
||||
],
|
||||
responses: [
|
||||
ok: {"Client Response", "application/json", API.Schemas.Client.Response}
|
||||
]
|
||||
)
|
||||
|
||||
# Delete a Client
|
||||
def delete(conn, %{"id" => id}) do
|
||||
subject = conn.assigns.subject
|
||||
|
||||
with {:ok, client} <- Clients.fetch_client_by_id(id, subject, preload: :online?),
|
||||
{:ok, client} <- Clients.delete_client(client, subject) do
|
||||
render(conn, :show, client: client)
|
||||
end
|
||||
end
|
||||
end
|
||||
51
elixir/apps/api/lib/api/controllers/client_json.ex
Normal file
51
elixir/apps/api/lib/api/controllers/client_json.ex
Normal file
@@ -0,0 +1,51 @@
|
||||
defmodule API.ClientJSON do
|
||||
alias API.Pagination
|
||||
alias Domain.Clients
|
||||
|
||||
@doc """
|
||||
Renders a list of Clients.
|
||||
"""
|
||||
def index(%{clients: clients, metadata: metadata}) do
|
||||
%{
|
||||
data: Enum.map(clients, &data/1),
|
||||
metadata: Pagination.metadata(metadata)
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Render a single Client
|
||||
"""
|
||||
def show(%{client: client}) do
|
||||
%{data: data(client)}
|
||||
end
|
||||
|
||||
defp data(%Clients.Client{} = client) do
|
||||
%{
|
||||
id: client.id,
|
||||
external_id: client.external_id,
|
||||
actor_id: client.actor_id,
|
||||
name: client.name,
|
||||
ipv4: client.ipv4,
|
||||
ipv6: client.ipv6,
|
||||
online: client.online?,
|
||||
last_seen_user_agent: client.last_seen_user_agent,
|
||||
last_seen_remote_ip: client.last_seen_remote_ip,
|
||||
last_seen_remote_ip_location_region: client.last_seen_remote_ip_location_region,
|
||||
last_seen_remote_ip_location_city: client.last_seen_remote_ip_location_city,
|
||||
last_seen_remote_ip_location_lat: client.last_seen_remote_ip_location_lat,
|
||||
last_seen_remote_ip_location_lon: client.last_seen_remote_ip_location_lon,
|
||||
last_seen_version: client.last_seen_version,
|
||||
last_seen_at: client.last_seen_at,
|
||||
device_serial: client.device_serial,
|
||||
device_uuid: client.device_uuid,
|
||||
identifier_for_vendor: client.identifier_for_vendor,
|
||||
firebase_installation_id: client.firebase_installation_id,
|
||||
verified_at: client.verified_at,
|
||||
verified_by: client.verified_by,
|
||||
verified_by_actor_id: client.verified_by_actor_id,
|
||||
verified_by_identity_id: client.verified_by_identity_id,
|
||||
created_at: client.inserted_at,
|
||||
updated_at: client.updated_at
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -15,6 +15,15 @@ defmodule API.FallbackController do
|
||||
|> render(:"401")
|
||||
end
|
||||
|
||||
def call(conn, {:error, {:unauthorized, details}}) do
|
||||
reason = Keyword.get(details, :reason, "Unauthorized")
|
||||
|
||||
conn
|
||||
|> put_status(:unauthorized)
|
||||
|> put_view(json: API.ErrorJSON)
|
||||
|> render(:"401", reason: reason)
|
||||
end
|
||||
|
||||
def call(conn, {:error, :bad_request}) do
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|
||||
@@ -40,6 +40,10 @@ defmodule API.Router do
|
||||
scope "/", API do
|
||||
pipe_through :api
|
||||
|
||||
resources "/clients", ClientController, except: [:new, :edit, :create]
|
||||
put "/clients/:id/verify", ClientController, :verify
|
||||
put "/clients/:id/unverify", ClientController, :unverify
|
||||
|
||||
resources "/resources", ResourceController, except: [:new, :edit]
|
||||
resources "/policies", PolicyController, except: [:new, :edit]
|
||||
|
||||
|
||||
311
elixir/apps/api/lib/api/schemas/client_schema.ex
Normal file
311
elixir/apps/api/lib/api/schemas/client_schema.ex
Normal file
@@ -0,0 +1,311 @@
|
||||
defmodule API.Schemas.Client do
|
||||
alias OpenApiSpex.Schema
|
||||
|
||||
defmodule GetSchema do
|
||||
require OpenApiSpex
|
||||
alias OpenApiSpex.Schema
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "Client",
|
||||
description: "Client",
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string, description: "Client ID"},
|
||||
actor_id: %Schema{type: :string, description: "Actor ID"},
|
||||
name: %Schema{
|
||||
type: :string,
|
||||
description: "Client Name"
|
||||
},
|
||||
ipv4: %Schema{
|
||||
type: :string,
|
||||
description: "Tunnel IPv4 Address of Client"
|
||||
},
|
||||
ipv6: %Schema{
|
||||
type: :string,
|
||||
description: "Tunnel IPv6 Address of Client"
|
||||
},
|
||||
online: %Schema{
|
||||
type: :boolean,
|
||||
description: "Online status of Client"
|
||||
},
|
||||
last_seen_user_agent: %Schema{
|
||||
type: :string,
|
||||
description: "Last seen user agent"
|
||||
},
|
||||
last_seen_remote_ip: %Schema{
|
||||
type: :string,
|
||||
description: "Last seen remote IP"
|
||||
},
|
||||
last_seen_remote_ip_location_region: %Schema{
|
||||
type: :string,
|
||||
description: "Last seen remote IP location region"
|
||||
},
|
||||
last_seen_remote_ip_location_city: %Schema{
|
||||
type: :string,
|
||||
description: "Last seen remote IP location city"
|
||||
},
|
||||
last_seen_remote_ip_location_lat: %Schema{
|
||||
type: :number,
|
||||
description: "Last seen remote IP location latitude"
|
||||
},
|
||||
last_seen_remote_ip_location_lon: %Schema{
|
||||
type: :number,
|
||||
description: "Last seen remote IP location longitude"
|
||||
},
|
||||
last_seen_version: %Schema{
|
||||
type: :string,
|
||||
description: "Last seen version"
|
||||
},
|
||||
last_seen_at: %Schema{
|
||||
type: :string,
|
||||
description: "Last seen at"
|
||||
},
|
||||
device_serial: %Schema{
|
||||
type: :string,
|
||||
description: "Device manufacturer serial number (unavailable for mobile devices)"
|
||||
},
|
||||
device_uuid: %Schema{
|
||||
type: :string,
|
||||
description: "Device manufacturer UUID (unavailable for mobile devices)"
|
||||
},
|
||||
identifier_for_vendor: %Schema{
|
||||
type: :string,
|
||||
description: "App installation ID (iOS only)"
|
||||
},
|
||||
firebase_installation_id: %Schema{
|
||||
type: :string,
|
||||
description: "Firebase installation ID (Android only)"
|
||||
},
|
||||
verified_at: %Schema{
|
||||
type: :string,
|
||||
description: "Client verification timestamp"
|
||||
},
|
||||
verified_by: %Schema{
|
||||
type: :string,
|
||||
description: "Client verification method",
|
||||
enum: [:system, :actor, :identity]
|
||||
},
|
||||
verified_by_actor_id: %Schema{
|
||||
type: :string,
|
||||
description: "Actor ID who verified the client"
|
||||
},
|
||||
verified_by_identity_id: %Schema{
|
||||
type: :string,
|
||||
description: "Identity ID who verified the client"
|
||||
},
|
||||
created_at: %Schema{
|
||||
type: :string,
|
||||
description: "Client creation timestamp"
|
||||
},
|
||||
updated_at: %Schema{
|
||||
type: :string,
|
||||
description: "Client update timestamp"
|
||||
}
|
||||
},
|
||||
required: [
|
||||
:id,
|
||||
:actor_id,
|
||||
:external_id,
|
||||
:name,
|
||||
:ipv4,
|
||||
:ipv6,
|
||||
:online,
|
||||
:last_seen_user_agent,
|
||||
:last_seen_remote_ip,
|
||||
:last_seen_remote_ip_location_region,
|
||||
:last_seen_remote_ip_location_city,
|
||||
:last_seen_remote_ip_location_lat,
|
||||
:last_seen_remote_ip_location_lon,
|
||||
:last_seen_version,
|
||||
:last_seen_at,
|
||||
:created_at,
|
||||
:updated_at
|
||||
],
|
||||
example: %{
|
||||
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
|
||||
"external_id" => "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c",
|
||||
"actor_id" => "6ecc106b-75c1-48a5-846c-14782180c1ff",
|
||||
"name" => "John's Macbook Air",
|
||||
"ipv4" => "100.64.0.1",
|
||||
"ipv6" => "fd00:2021:1111::1",
|
||||
"online" => true,
|
||||
"last_seen_user_agent" => "Mac OS/15.1.1 connlib/1.4.5 (arm64; 24.1.0)",
|
||||
"last_seen_remote_ip" => "1.2.3.4",
|
||||
"last_seen_remote_ip_location_region" => "California",
|
||||
"last_seen_remote_ip_location_city" => "San Francisco",
|
||||
"last_seen_remote_ip_location_lat" => 37.7749,
|
||||
"last_seen_remote_ip_location_lon" => -122.4194,
|
||||
"last_seen_version" => "1.4.5",
|
||||
"last_seen_at" => "2025-01-01T00:00:00Z",
|
||||
"created_at" => "2025-01-01T00:00:00Z",
|
||||
"updated_at" => "2025-01-01T00:00:00Z"
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
defmodule PutSchema do
|
||||
require OpenApiSpex
|
||||
alias OpenApiSpex.Schema
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "ClientPut",
|
||||
description: "Put schema for updating a single Client",
|
||||
type: :object,
|
||||
properties: %{
|
||||
name: %Schema{
|
||||
type: :string,
|
||||
description: "Client Name"
|
||||
}
|
||||
},
|
||||
required: [:name],
|
||||
example: %{
|
||||
"name" => "John's Macbook Air"
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
defmodule Request do
|
||||
require OpenApiSpex
|
||||
alias OpenApiSpex.Schema
|
||||
alias API.Schemas.Client
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "ClientPutRequest",
|
||||
description: "PUT body for updating a Client",
|
||||
type: :object,
|
||||
properties: %{
|
||||
client: Client.PutSchema
|
||||
},
|
||||
required: [:client],
|
||||
example: %{
|
||||
"client" => %{
|
||||
"name" => "John's Macbook Air"
|
||||
}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
defmodule Response do
|
||||
require OpenApiSpex
|
||||
alias OpenApiSpex.Schema
|
||||
alias API.Schemas.Client
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "ClientResponse",
|
||||
description: "Response schema for single Client",
|
||||
type: :object,
|
||||
properties: %{
|
||||
data: Client.GetSchema
|
||||
},
|
||||
example: %{
|
||||
"data" => %{
|
||||
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
|
||||
"external_id" => "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c",
|
||||
"actor_id" => "6ecc106b-75c1-48a5-846c-14782180c1ff",
|
||||
"name" => "John's Macbook Air",
|
||||
"ipv4" => "100.64.0.1",
|
||||
"ipv6" => "fd00:2021:1111::1",
|
||||
"online" => true,
|
||||
"last_seen_user_agent" => "Mac OS/15.1.1 connlib/1.4.5 (arm64; 24.1.0)",
|
||||
"last_seen_remote_ip" => "1.2.3.4",
|
||||
"last_seen_remote_ip_location_region" => "California",
|
||||
"last_seen_remote_ip_location_city" => "San Francisco",
|
||||
"last_seen_remote_ip_location_lat" => 37.7749,
|
||||
"last_seen_remote_ip_location_lon" => -122.4194,
|
||||
"last_seen_version" => "1.4.5",
|
||||
"last_seen_at" => "2025-01-01T00:00:00Z",
|
||||
"device_serial" => "GCCFX0DBQ6L5",
|
||||
"device_uuid" => "7A461FF9-0BE2-64A9-A418-539D9A21827B",
|
||||
"identifier_for_vendor" => nil,
|
||||
"firebase_installation_id" => nil,
|
||||
"verified_at" => "2025-01-01T00:00:00Z",
|
||||
"verified_by" => "identity",
|
||||
"verified_by_actor_id" => nil,
|
||||
"verified_by_identity_id" => "6ecc106b-75c1-48a5-846c-14782180c1ff",
|
||||
"created_at" => "2025-01-01T00:00:00Z",
|
||||
"updated_at" => "2025-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
defmodule ListResponse do
|
||||
require OpenApiSpex
|
||||
alias OpenApiSpex.Schema
|
||||
alias API.Schemas.Client
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "ClientsResponse",
|
||||
description: "Response schema for multiple Clients",
|
||||
type: :object,
|
||||
properties: %{
|
||||
data: %Schema{description: "Clients details", type: :array, items: Client.GetSchema},
|
||||
metadata: %Schema{description: "Pagination metadata", type: :object}
|
||||
},
|
||||
example: %{
|
||||
"data" => [
|
||||
%{
|
||||
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
|
||||
"external_id" => "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c",
|
||||
"actor_id" => "6ecc106b-75c1-48a5-846c-14782180c1ff",
|
||||
"name" => "John's Macbook Air",
|
||||
"ipv4" => "100.64.0.1",
|
||||
"ipv6" => "fd00:2021:1111::1",
|
||||
"online" => true,
|
||||
"last_seen_user_agent" => "Mac OS/15.1.1 connlib/1.4.5 (arm64; 24.1.0)",
|
||||
"last_seen_remote_ip" => "1.2.3.4",
|
||||
"last_seen_remote_ip_location_region" => "California",
|
||||
"last_seen_remote_ip_location_city" => "San Francisco",
|
||||
"last_seen_remote_ip_location_lat" => 37.7749,
|
||||
"last_seen_remote_ip_location_lon" => -122.4194,
|
||||
"last_seen_version" => "1.4.5",
|
||||
"last_seen_at" => "2025-01-01T00:00:00Z",
|
||||
"device_serial" => "GCCFX0DBQ6L5",
|
||||
"device_uuid" => "7A461FF9-0BE2-64A9-A418-539D9A21827B",
|
||||
"identifier_for_vendor" => nil,
|
||||
"firebase_installation_id" => nil,
|
||||
"verified_at" => "2025-01-01T00:00:00Z",
|
||||
"verified_by" => "identity",
|
||||
"verified_by_actor_id" => nil,
|
||||
"verified_by_identity_id" => "6ecc106b-75c1-48a5-846c-14782180c1ff",
|
||||
"created_at" => "2025-01-01T00:00:00Z",
|
||||
"updated_at" => "2025-01-01T00:00:00Z"
|
||||
},
|
||||
%{
|
||||
"id" => "9a7f82f-831a-4a9d-8f17-c66c2bb6e205",
|
||||
"external_id" => "6c37c0042f40bbb16e007d0d6c8e77c0ac2cab3cc3b923c42d1157a934e436ac",
|
||||
"actor_id" => "2ecc106b-75c1-48a5-846c-14782180c1ff",
|
||||
"name" => "iPad",
|
||||
"ipv4" => "100.64.0.2",
|
||||
"ipv6" => "fd00:2021:1111::2",
|
||||
"online" => false,
|
||||
"last_seen_user_agent" => "iOS/18.3.1 connlib/1.4.6 (24.3.0)",
|
||||
"last_seen_remote_ip" => "1.2.3.4",
|
||||
"last_seen_remote_ip_location_region" => "California",
|
||||
"last_seen_remote_ip_location_city" => "San Francisco",
|
||||
"last_seen_remote_ip_location_lat" => 37.7749,
|
||||
"last_seen_remote_ip_location_lon" => -122.4194,
|
||||
"last_seen_version" => "1.4.6",
|
||||
"last_seen_at" => "2025-01-01T00:00:00Z",
|
||||
"device_serial" => nil,
|
||||
"device_uuid" => nil,
|
||||
"identifier_for_vendor" => "7A461FF9-0BE2-64A9-A418-539D9A21827B",
|
||||
"firebase_installation_id" => nil,
|
||||
"verified_at" => nil,
|
||||
"verified_by" => nil,
|
||||
"verified_by_actor_id" => nil,
|
||||
"verified_by_identity_id" => nil,
|
||||
"created_at" => "2025-01-01T00:00:00Z",
|
||||
"updated_at" => "2025-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"metadata" => %{
|
||||
"limit" => 2,
|
||||
"total" => 100,
|
||||
"prev_page" => "123123425",
|
||||
"next_page" => "98776234123"
|
||||
}
|
||||
}
|
||||
})
|
||||
end
|
||||
end
|
||||
@@ -29,12 +29,12 @@ defmodule API.Schemas.Gateway do
|
||||
description: "Online status of Gateway"
|
||||
}
|
||||
},
|
||||
required: [:name, :type],
|
||||
required: [:id, :name, :ipv4, :ipv6, :online],
|
||||
example: %{
|
||||
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
|
||||
"name" => "vpc-us-east",
|
||||
"ipv4" => "1.2.3.4",
|
||||
"ipv6" => "",
|
||||
"ipv4" => "100.64.0.1",
|
||||
"ipv6" => "fd00:2021:1111::1",
|
||||
"online" => true
|
||||
}
|
||||
})
|
||||
|
||||
297
elixir/apps/api/test/api/controllers/client_controller_test.exs
Normal file
297
elixir/apps/api/test/api/controllers/client_controller_test.exs
Normal file
@@ -0,0 +1,297 @@
|
||||
defmodule API.ClientControllerTest do
|
||||
use API.ConnCase, async: true
|
||||
alias Domain.Clients.Client
|
||||
|
||||
setup do
|
||||
account = Fixtures.Accounts.create_account()
|
||||
actor = Fixtures.Actors.create_actor(type: :api_client, account: account)
|
||||
identity = Fixtures.Auth.create_identity(account: account, actor: actor)
|
||||
client = Fixtures.Clients.create_client(account: account)
|
||||
subject = Fixtures.Auth.create_subject(identity: identity)
|
||||
|
||||
%{
|
||||
account: account,
|
||||
actor: actor,
|
||||
client: client,
|
||||
identity: identity,
|
||||
subject: subject
|
||||
}
|
||||
end
|
||||
|
||||
describe "index/2" do
|
||||
test "returns error when not authorized", %{conn: conn, client: client} do
|
||||
conn = get(conn, ~p"/clients/#{client}")
|
||||
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
|
||||
end
|
||||
|
||||
test "lists all clients", %{
|
||||
conn: conn,
|
||||
actor: actor,
|
||||
client: client,
|
||||
account: account
|
||||
} do
|
||||
clients =
|
||||
for _ <- 1..3,
|
||||
do: Fixtures.Clients.create_client(%{account: account})
|
||||
|
||||
clients = [client | clients]
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> authorize_conn(actor)
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> get(~p"/clients")
|
||||
|
||||
assert %{
|
||||
"data" => data,
|
||||
"metadata" => %{
|
||||
"count" => count,
|
||||
"limit" => limit,
|
||||
"next_page" => next_page,
|
||||
"prev_page" => prev_page
|
||||
}
|
||||
} = json_response(conn, 200)
|
||||
|
||||
# client was created in setup
|
||||
assert count == 4
|
||||
assert limit == 50
|
||||
assert is_nil(next_page)
|
||||
assert is_nil(prev_page)
|
||||
|
||||
data_ids = Enum.map(data, & &1["id"])
|
||||
client_ids = Enum.map(clients, & &1.id)
|
||||
|
||||
assert equal_ids?(data_ids, client_ids)
|
||||
end
|
||||
|
||||
test "lists clients with limit", %{
|
||||
conn: conn,
|
||||
actor: actor,
|
||||
client: client,
|
||||
account: account
|
||||
} do
|
||||
clients =
|
||||
for _ <- 1..3,
|
||||
do: Fixtures.Clients.create_client(%{account: account})
|
||||
|
||||
clients = [client | clients]
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> authorize_conn(actor)
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> get(~p"/clients", limit: "2")
|
||||
|
||||
assert %{
|
||||
"data" => data,
|
||||
"metadata" => %{
|
||||
"count" => count,
|
||||
"limit" => limit,
|
||||
"next_page" => next_page,
|
||||
"prev_page" => prev_page
|
||||
}
|
||||
} = json_response(conn, 200)
|
||||
|
||||
assert limit == 2
|
||||
# client was created in setup
|
||||
assert count == 4
|
||||
refute is_nil(next_page)
|
||||
assert is_nil(prev_page)
|
||||
|
||||
data_ids = Enum.map(data, & &1["id"]) |> MapSet.new()
|
||||
client_ids = Enum.map(clients, & &1.id) |> MapSet.new()
|
||||
|
||||
assert MapSet.subset?(data_ids, client_ids)
|
||||
end
|
||||
end
|
||||
|
||||
describe "show/2" do
|
||||
test "returns error when not authorized", %{
|
||||
conn: conn,
|
||||
client: client
|
||||
} do
|
||||
conn = get(conn, ~p"/clients/#{client.id}")
|
||||
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
|
||||
end
|
||||
|
||||
test "returns a single client", %{
|
||||
conn: conn,
|
||||
actor: actor,
|
||||
client: client
|
||||
} do
|
||||
conn =
|
||||
conn
|
||||
|> authorize_conn(actor)
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> get(~p"/clients/#{client.id}")
|
||||
|
||||
assert json_response(conn, 200) == %{
|
||||
"data" => %{
|
||||
"id" => client.id,
|
||||
"name" => client.name,
|
||||
"ipv4" => "#{client.ipv4}",
|
||||
"ipv6" => "#{client.ipv6}",
|
||||
"actor_id" => client.actor_id,
|
||||
"created_at" => client.inserted_at && DateTime.to_iso8601(client.inserted_at),
|
||||
"device_serial" => client.device_serial,
|
||||
"device_uuid" => client.device_uuid,
|
||||
"external_id" => client.external_id,
|
||||
"firebase_installation_id" => client.firebase_installation_id,
|
||||
"identifier_for_vendor" => client.identifier_for_vendor,
|
||||
"last_seen_at" =>
|
||||
client.last_seen_at && DateTime.to_iso8601(client.last_seen_at),
|
||||
"last_seen_remote_ip" => "#{client.last_seen_remote_ip}",
|
||||
"last_seen_remote_ip_location_city" => client.last_seen_remote_ip_location_city,
|
||||
"last_seen_remote_ip_location_lat" => client.last_seen_remote_ip_location_lat,
|
||||
"last_seen_remote_ip_location_lon" => client.last_seen_remote_ip_location_lon,
|
||||
"last_seen_remote_ip_location_region" =>
|
||||
client.last_seen_remote_ip_location_region,
|
||||
"last_seen_user_agent" => client.last_seen_user_agent,
|
||||
"last_seen_version" => client.last_seen_version,
|
||||
"online" => client.online?,
|
||||
"updated_at" => client.updated_at && DateTime.to_iso8601(client.updated_at),
|
||||
"verified_at" => client.verified_at && DateTime.to_iso8601(client.verified_at),
|
||||
"verified_by" => client.verified_by,
|
||||
"verified_by_actor_id" => client.verified_by_actor_id,
|
||||
"verified_by_identity_id" => client.verified_by_identity_id
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
describe "update/2" do
|
||||
test "returns error when not authorized", %{conn: conn, client: client} do
|
||||
conn = put(conn, ~p"/clients/#{client}", %{})
|
||||
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
|
||||
end
|
||||
|
||||
test "returns error on empty params/body", %{conn: conn, actor: actor, client: client} do
|
||||
conn =
|
||||
conn
|
||||
|> authorize_conn(actor)
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> put(~p"/clients/#{client}")
|
||||
|
||||
assert resp = json_response(conn, 400)
|
||||
assert resp == %{"error" => %{"reason" => "Bad Request"}}
|
||||
end
|
||||
|
||||
test "updates a client", %{conn: conn, actor: actor, client: client} do
|
||||
attrs = %{"name" => "Updated Client"}
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> authorize_conn(actor)
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> put(~p"/clients/#{client}", client: attrs)
|
||||
|
||||
assert resp = json_response(conn, 200)
|
||||
|
||||
assert resp["data"]["id"] == client.id
|
||||
assert resp["data"]["name"] == attrs["name"]
|
||||
end
|
||||
end
|
||||
|
||||
describe "verify/2" do
|
||||
test "returns error when not authorized", %{conn: conn, client: client} do
|
||||
conn = put(conn, ~p"/clients/#{client}/verify", %{})
|
||||
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
|
||||
end
|
||||
|
||||
test "verifies a client", %{conn: conn, actor: actor, client: client} do
|
||||
conn =
|
||||
conn
|
||||
|> authorize_conn(actor)
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> put(~p"/clients/#{client}/verify")
|
||||
|
||||
assert resp = json_response(conn, 200)
|
||||
|
||||
assert resp["data"]["id"] == client.id
|
||||
assert resp["data"]["verified_at"]
|
||||
assert resp["data"]["verified_by"]
|
||||
assert resp["data"]["verified_by_actor_id"]
|
||||
refute resp["data"]["verified_by_identity_id"]
|
||||
end
|
||||
end
|
||||
|
||||
describe "unverify/2" do
|
||||
test "returns error when not authorized", %{conn: conn, client: client} do
|
||||
conn = put(conn, ~p"/clients/#{client}/verify", %{})
|
||||
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
|
||||
end
|
||||
|
||||
test "unverifies a client", %{conn: conn, actor: actor, client: client} do
|
||||
conn =
|
||||
conn
|
||||
|> authorize_conn(actor)
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> put(~p"/clients/#{client}/unverify")
|
||||
|
||||
assert resp = json_response(conn, 200)
|
||||
|
||||
assert resp["data"]["id"] == client.id
|
||||
refute resp["data"]["verified_at"]
|
||||
refute resp["data"]["verified_by"]
|
||||
refute resp["data"]["verified_by_actor_id"]
|
||||
refute resp["data"]["verified_by_identity_id"]
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete/2" do
|
||||
test "returns error when not authorized", %{
|
||||
conn: conn,
|
||||
client: client
|
||||
} do
|
||||
conn = delete(conn, ~p"/clients/#{client.id}")
|
||||
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
|
||||
end
|
||||
|
||||
test "deletes a client", %{
|
||||
conn: conn,
|
||||
actor: actor,
|
||||
client: client
|
||||
} do
|
||||
conn =
|
||||
conn
|
||||
|> authorize_conn(actor)
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> delete(~p"/clients/#{client}")
|
||||
|
||||
assert json_response(conn, 200) == %{
|
||||
"data" => %{
|
||||
"id" => client.id,
|
||||
"name" => client.name,
|
||||
"ipv4" => "#{client.ipv4}",
|
||||
"ipv6" => "#{client.ipv6}",
|
||||
"actor_id" => client.actor_id,
|
||||
"created_at" => client.inserted_at && DateTime.to_iso8601(client.inserted_at),
|
||||
"device_serial" => client.device_serial,
|
||||
"device_uuid" => client.device_uuid,
|
||||
"external_id" => client.external_id,
|
||||
"firebase_installation_id" => client.firebase_installation_id,
|
||||
"identifier_for_vendor" => client.identifier_for_vendor,
|
||||
"last_seen_at" =>
|
||||
client.last_seen_at && DateTime.to_iso8601(client.last_seen_at),
|
||||
"last_seen_remote_ip" => "#{client.last_seen_remote_ip}",
|
||||
"last_seen_remote_ip_location_city" => client.last_seen_remote_ip_location_city,
|
||||
"last_seen_remote_ip_location_lat" => client.last_seen_remote_ip_location_lat,
|
||||
"last_seen_remote_ip_location_lon" => client.last_seen_remote_ip_location_lon,
|
||||
"last_seen_remote_ip_location_region" =>
|
||||
client.last_seen_remote_ip_location_region,
|
||||
"last_seen_user_agent" => client.last_seen_user_agent,
|
||||
"last_seen_version" => client.last_seen_version,
|
||||
"online" => nil,
|
||||
"updated_at" => client.updated_at && DateTime.to_iso8601(client.updated_at),
|
||||
"verified_at" => client.verified_at && DateTime.to_iso8601(client.verified_at),
|
||||
"verified_by" => client.verified_by,
|
||||
"verified_by_actor_id" => client.verified_by_actor_id,
|
||||
"verified_by_identity_id" => client.verified_by_identity_id
|
||||
}
|
||||
}
|
||||
|
||||
assert client = Repo.get(Client, client.id)
|
||||
assert client.deleted_at
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -19,7 +19,8 @@ defmodule Domain.Clients.Authorizer do
|
||||
def list_permissions_for_role(:api_client) do
|
||||
[
|
||||
manage_own_clients_permission(),
|
||||
manage_clients_permission()
|
||||
manage_clients_permission(),
|
||||
verify_clients_permission()
|
||||
]
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user