diff --git a/lib/plausible/auth/api_key.ex b/lib/plausible/auth/api_key.ex
index edff9ad5a..adb77ddf8 100644
--- a/lib/plausible/auth/api_key.ex
+++ b/lib/plausible/auth/api_key.ex
@@ -24,6 +24,7 @@ defmodule Plausible.Auth.ApiKey do
|> validate_required(@required)
|> generate_key()
|> process_key()
+ |> unique_constraint(:key_hash, error_key: :key)
end
def update(schema, attrs \\ %{}) do
diff --git a/lib/plausible_web/templates/auth/new_api_key.html.eex b/lib/plausible_web/templates/auth/new_api_key.html.eex
index e45e4eb9c..3e2b1bf53 100644
--- a/lib/plausible_web/templates/auth/new_api_key.html.eex
+++ b/lib/plausible_web/templates/auth/new_api_key.html.eex
@@ -11,9 +11,10 @@
<%= label f, :key, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= text_input f, :key, id: "key-input", class: "dark:text-gray-300 shadow-sm bg-gray-50 dark:bg-gray-850 focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md pr-16", readonly: "readonly" %>
-
- COPY
-
+
+ COPY
+
+ <%= error_tag f, :key %>
Make sure to store the key in a secure place. Once created, we will not be able to show it again.
diff --git a/priv/repo/migrations/20230516131041_add_unique_index_to_api_keys.exs b/priv/repo/migrations/20230516131041_add_unique_index_to_api_keys.exs
new file mode 100644
index 000000000..f15b92ab0
--- /dev/null
+++ b/priv/repo/migrations/20230516131041_add_unique_index_to_api_keys.exs
@@ -0,0 +1,7 @@
+defmodule Plausible.Repo.Migrations.AddUniqueIndexToApiKeys do
+ use Ecto.Migration
+
+ def change do
+ create unique_index(:api_keys, :key_hash)
+ end
+end
diff --git a/test/plausible_web/controllers/auth_controller_test.exs b/test/plausible_web/controllers/auth_controller_test.exs
index 8edf0c0fc..73514abe9 100644
--- a/test/plausible_web/controllers/auth_controller_test.exs
+++ b/test/plausible_web/controllers/auth_controller_test.exs
@@ -709,6 +709,49 @@ defmodule PlausibleWeb.AuthControllerTest do
setup [:create_user, :log_in]
import Ecto.Query
+ test "can create an API key", %{conn: conn, user: user} do
+ site = insert(:site)
+ insert(:site_membership, site: site, user: user, role: "owner")
+
+ conn =
+ post(conn, "/settings/api-keys", %{
+ "api_key" => %{
+ "user_id" => user.id,
+ "name" => "all your code are belong to us",
+ "key" => "swordfish"
+ }
+ })
+
+ key = Plausible.Auth.ApiKey |> where(user_id: ^user.id) |> Repo.one()
+ assert conn.status == 302
+ assert key.name == "all your code are belong to us"
+ end
+
+ test "cannot create a duplicate API key", %{conn: conn, user: user} do
+ site = insert(:site)
+ insert(:site_membership, site: site, user: user, role: "owner")
+
+ conn =
+ post(conn, "/settings/api-keys", %{
+ "api_key" => %{
+ "user_id" => user.id,
+ "name" => "all your code are belong to us",
+ "key" => "swordfish"
+ }
+ })
+
+ conn2 =
+ post(conn, "/settings/api-keys", %{
+ "api_key" => %{
+ "user_id" => user.id,
+ "name" => "all your code are belong to us",
+ "key" => "swordfish"
+ }
+ })
+
+ assert html_response(conn2, 200) =~ "has already been taken"
+ end
+
test "can't create api key into another site", %{conn: conn, user: me} do
my_site = insert(:site)
insert(:site_membership, site: my_site, user: me, role: "owner")