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:
Jamil
2025-03-05 00:37:01 +00:00
committed by GitHub
parent e064cf5821
commit c3a9bac465
8 changed files with 844 additions and 4 deletions

View 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

View 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

View File

@@ -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)

View File

@@ -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]

View 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

View File

@@ -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
}
})

View 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

View File

@@ -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