From d09bcf646e8a79c4c36d3787cef3a19b375b9ee8 Mon Sep 17 00:00:00 2001 From: Ivan Reshetnikov Date: Thu, 5 Sep 2024 02:27:33 +0500 Subject: [PATCH] feat: generate modules for openid apps --- assets/css/admin.css | 1 + lib/comfycamp/accounts/user.ex | 6 + lib/comfycamp/sso.ex | 104 ++++++++++++++++++ lib/comfycamp/sso/oidc_app.ex | 23 ++++ .../components/layouts/admin.html.heex | 4 +- .../controllers/oidc_app_controller.ex | 76 +++++++++++++ .../controllers/oidc_app_html.ex | 13 +++ .../controllers/oidc_app_html/edit.html.heex | 10 ++ .../controllers/oidc_app_html/index.html.heex | 28 +++++ .../controllers/oidc_app_html/new.html.heex | 10 ++ .../oidc_app_html/oidc_app_form.html.heex | 14 +++ .../controllers/oidc_app_html/show.html.heex | 20 ++++ lib/comfycamp_web/router.ex | 1 + .../20240904204356_create_oidc_apps.exs | 14 +++ test/comfycamp/sso_test.exs | 76 +++++++++++++ .../controllers/oidc_app_controller_test.exs | 100 +++++++++++++++++ test/support/conn_case.ex | 20 ++++ test/support/fixtures/sso_fixtures.ex | 23 ++++ 18 files changed, 541 insertions(+), 2 deletions(-) create mode 100644 lib/comfycamp/sso.ex create mode 100644 lib/comfycamp/sso/oidc_app.ex create mode 100644 lib/comfycamp_web/controllers/oidc_app_controller.ex create mode 100644 lib/comfycamp_web/controllers/oidc_app_html.ex create mode 100644 lib/comfycamp_web/controllers/oidc_app_html/edit.html.heex create mode 100644 lib/comfycamp_web/controllers/oidc_app_html/index.html.heex create mode 100644 lib/comfycamp_web/controllers/oidc_app_html/new.html.heex create mode 100644 lib/comfycamp_web/controllers/oidc_app_html/oidc_app_form.html.heex create mode 100644 lib/comfycamp_web/controllers/oidc_app_html/show.html.heex create mode 100644 priv/repo/migrations/20240904204356_create_oidc_apps.exs create mode 100644 test/comfycamp/sso_test.exs create mode 100644 test/comfycamp_web/controllers/oidc_app_controller_test.exs create mode 100644 test/support/fixtures/sso_fixtures.ex diff --git a/assets/css/admin.css b/assets/css/admin.css index 1689f45..2eef744 100644 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -27,6 +27,7 @@ border-radius: 0 0 8px 8px; } +.admin-panel h1, .admin-panel h3 { margin-top: 0; } diff --git a/lib/comfycamp/accounts/user.ex b/lib/comfycamp/accounts/user.ex index b0279e7..6390f49 100644 --- a/lib/comfycamp/accounts/user.ex +++ b/lib/comfycamp/accounts/user.ex @@ -137,6 +137,12 @@ defmodule Comfycamp.Accounts.User do |> validate_required([:is_approved]) end + def admin_status_changeset(user, attrs) do + user + |> cast(attrs, [:is_admin]) + |> validate_required([:is_admin]) + end + @doc """ Verifies the password. diff --git a/lib/comfycamp/sso.ex b/lib/comfycamp/sso.ex new file mode 100644 index 0000000..b4e6174 --- /dev/null +++ b/lib/comfycamp/sso.ex @@ -0,0 +1,104 @@ +defmodule Comfycamp.SSO do + @moduledoc """ + The SSO context. + """ + + import Ecto.Query, warn: false + alias Comfycamp.Repo + + alias Comfycamp.SSO.OIDCApp + + @doc """ + Returns the list of oidc_apps. + + ## Examples + + iex> list_oidc_apps() + [%OIDCApp{}, ...] + + """ + def list_oidc_apps do + Repo.all(OIDCApp) + end + + @doc """ + Gets a single oidc_app. + + Raises `Ecto.NoResultsError` if the Oidc app does not exist. + + ## Examples + + iex> get_oidc_app!(123) + %OIDCApp{} + + iex> get_oidc_app!(456) + ** (Ecto.NoResultsError) + + """ + def get_oidc_app!(id), do: Repo.get!(OIDCApp, id) + + @doc """ + Creates a oidc_app. + + ## Examples + + iex> create_oidc_app(%{field: value}) + {:ok, %OIDCApp{}} + + iex> create_oidc_app(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_oidc_app(attrs \\ %{}) do + %OIDCApp{} + |> OIDCApp.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a oidc_app. + + ## Examples + + iex> update_oidc_app(oidc_app, %{field: new_value}) + {:ok, %OIDCApp{}} + + iex> update_oidc_app(oidc_app, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_oidc_app(%OIDCApp{} = oidc_app, attrs) do + oidc_app + |> OIDCApp.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a oidc_app. + + ## Examples + + iex> delete_oidc_app(oidc_app) + {:ok, %OIDCApp{}} + + iex> delete_oidc_app(oidc_app) + {:error, %Ecto.Changeset{}} + + """ + def delete_oidc_app(%OIDCApp{} = oidc_app) do + Repo.delete(oidc_app) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking oidc_app changes. + + ## Examples + + iex> change_oidc_app(oidc_app) + %Ecto.Changeset{data: %OIDCApp{}} + + """ + def change_oidc_app(%OIDCApp{} = oidc_app, attrs \\ %{}) do + OIDCApp.changeset(oidc_app, attrs) + end +end diff --git a/lib/comfycamp/sso/oidc_app.ex b/lib/comfycamp/sso/oidc_app.ex new file mode 100644 index 0000000..2686173 --- /dev/null +++ b/lib/comfycamp/sso/oidc_app.ex @@ -0,0 +1,23 @@ +defmodule Comfycamp.SSO.OIDCApp do + use Ecto.Schema + import Ecto.Changeset + + schema "oidc_apps" do + field :enabled, :boolean, default: false + field :name, :string + field :client_id, :string + field :client_secret, :string + + timestamps(type: :utc_datetime) + end + + @doc false + def changeset(oidc_app, attrs) do + oidc_app + |> cast(attrs, [:name, :client_id, :client_secret, :enabled]) + |> validate_required([:name, :client_id, :client_secret, :enabled]) + |> validate_length(:name, min: 2) + |> validate_length(:client_id, min: 8) + |> validate_length(:client_secret, min: 12) + end +end diff --git a/lib/comfycamp_web/components/layouts/admin.html.heex b/lib/comfycamp_web/components/layouts/admin.html.heex index 427dfb8..9d34ee4 100644 --- a/lib/comfycamp_web/components/layouts/admin.html.heex +++ b/lib/comfycamp_web/components/layouts/admin.html.heex @@ -22,8 +22,8 @@
  • - <.link href={~p"/admin/services"}> - Сервисы + <.link href={~p"/admin/oidc_apps"}> + OpenID
  • diff --git a/lib/comfycamp_web/controllers/oidc_app_controller.ex b/lib/comfycamp_web/controllers/oidc_app_controller.ex new file mode 100644 index 0000000..1744907 --- /dev/null +++ b/lib/comfycamp_web/controllers/oidc_app_controller.ex @@ -0,0 +1,76 @@ +defmodule ComfycampWeb.OIDCAppController do + use ComfycampWeb, :controller + + alias Comfycamp.SSO + alias Comfycamp.SSO.OIDCApp + + def index(conn, _params) do + oidc_apps = SSO.list_oidc_apps() + + conn + |> put_layout(html: :admin) + |> render(:index, oidc_apps: oidc_apps) + end + + def new(conn, _params) do + changeset = SSO.change_oidc_app(%OIDCApp{}) + + conn + |> put_layout(html: :admin) + |> render(:new, changeset: changeset) + end + + def create(conn, %{"oidc_app" => oidc_app_params}) do + case SSO.create_oidc_app(oidc_app_params) do + {:ok, oidc_app} -> + conn + |> put_flash(:info, "Oidc app created successfully.") + |> redirect(to: ~p"/admin/oidc_apps/#{oidc_app}") + + {:error, %Ecto.Changeset{} = changeset} -> + conn + |> put_layout(html: :admin) + |> render(:new, changeset: changeset) + end + end + + def show(conn, %{"id" => id}) do + oidc_app = SSO.get_oidc_app!(id) + + conn + |> put_layout(html: :admin) + |> render(:show, oidc_app: oidc_app) + end + + def edit(conn, %{"id" => id}) do + oidc_app = SSO.get_oidc_app!(id) + changeset = SSO.change_oidc_app(oidc_app) + + conn + |> put_layout(html: :admin) + |> render(:edit, oidc_app: oidc_app, changeset: changeset) + end + + def update(conn, %{"id" => id, "oidc_app" => oidc_app_params}) do + oidc_app = SSO.get_oidc_app!(id) + + case SSO.update_oidc_app(oidc_app, oidc_app_params) do + {:ok, oidc_app} -> + conn + |> put_flash(:info, "Oidc app updated successfully.") + |> redirect(to: ~p"/admin/oidc_apps/#{oidc_app}") + + {:error, %Ecto.Changeset{} = changeset} -> + render(conn, :edit, oidc_app: oidc_app, changeset: changeset) + end + end + + def delete(conn, %{"id" => id}) do + oidc_app = SSO.get_oidc_app!(id) + {:ok, _oidc_app} = SSO.delete_oidc_app(oidc_app) + + conn + |> put_flash(:info, "Oidc app deleted successfully.") + |> redirect(to: ~p"/admin/oidc_apps") + end +end diff --git a/lib/comfycamp_web/controllers/oidc_app_html.ex b/lib/comfycamp_web/controllers/oidc_app_html.ex new file mode 100644 index 0000000..ecee3a8 --- /dev/null +++ b/lib/comfycamp_web/controllers/oidc_app_html.ex @@ -0,0 +1,13 @@ +defmodule ComfycampWeb.OIDCAppHTML do + use ComfycampWeb, :html + + embed_templates "oidc_app_html/*" + + @doc """ + Renders a oidc_app form. + """ + attr :changeset, Ecto.Changeset, required: true + attr :action, :string, required: true + + def oidc_app_form(assigns) +end diff --git a/lib/comfycamp_web/controllers/oidc_app_html/edit.html.heex b/lib/comfycamp_web/controllers/oidc_app_html/edit.html.heex new file mode 100644 index 0000000..e25f823 --- /dev/null +++ b/lib/comfycamp_web/controllers/oidc_app_html/edit.html.heex @@ -0,0 +1,10 @@ +
    + <.header> + Edit Oidc app <%= @oidc_app.id %> + <:subtitle>Use this form to manage oidc_app records in your database. + + + <.oidc_app_form changeset={@changeset} action={~p"/admin/oidc_apps/#{@oidc_app}"} /> + + <.back navigate={~p"/admin/oidc_apps"}>Back to oidc_apps +
    diff --git a/lib/comfycamp_web/controllers/oidc_app_html/index.html.heex b/lib/comfycamp_web/controllers/oidc_app_html/index.html.heex new file mode 100644 index 0000000..875f345 --- /dev/null +++ b/lib/comfycamp_web/controllers/oidc_app_html/index.html.heex @@ -0,0 +1,28 @@ +
    + <.header> + Listing Oidc apps + <:actions> + <.link href={~p"/admin/oidc_apps/new"}> + <.button>New Oidc app + + + + + <.table id="oidc_apps" rows={@oidc_apps} row_click={&JS.navigate(~p"/admin/oidc_apps/#{&1}")}> + <:col :let={oidc_app} label="Name"><%= oidc_app.name %> + <:col :let={oidc_app} label="Client"><%= oidc_app.client_id %> + <:col :let={oidc_app} label="Client secret"><%= oidc_app.client_secret %> + <:col :let={oidc_app} label="Enabled"><%= oidc_app.enabled %> + <:action :let={oidc_app}> +
    + <.link navigate={~p"/admin/oidc_apps/#{oidc_app}"}>Show +
    + <.link navigate={~p"/admin/oidc_apps/#{oidc_app}/edit"}>Edit + + <:action :let={oidc_app}> + <.link href={~p"/admin/oidc_apps/#{oidc_app}"} method="delete" data-confirm="Are you sure?"> + Delete + + + +
    diff --git a/lib/comfycamp_web/controllers/oidc_app_html/new.html.heex b/lib/comfycamp_web/controllers/oidc_app_html/new.html.heex new file mode 100644 index 0000000..802ec6d --- /dev/null +++ b/lib/comfycamp_web/controllers/oidc_app_html/new.html.heex @@ -0,0 +1,10 @@ +
    + <.header> + New Oidc app + <:subtitle>Use this form to manage oidc_app records in your database. + + + <.oidc_app_form changeset={@changeset} action={~p"/admin/oidc_apps"} /> + + <.back navigate={~p"/admin/oidc_apps"}>Back to oidc_apps +
    diff --git a/lib/comfycamp_web/controllers/oidc_app_html/oidc_app_form.html.heex b/lib/comfycamp_web/controllers/oidc_app_html/oidc_app_form.html.heex new file mode 100644 index 0000000..b44d18b --- /dev/null +++ b/lib/comfycamp_web/controllers/oidc_app_html/oidc_app_form.html.heex @@ -0,0 +1,14 @@ +
    + <.simple_form :let={f} for={@changeset} action={@action}> + <.error :if={@changeset.action}> + Oops, something went wrong! Please check the errors below. + + <.input field={f[:name]} type="text" label="Name" /> + <.input field={f[:client_id]} type="text" label="Client" /> + <.input field={f[:client_secret]} type="text" label="Client secret" /> + <.input field={f[:enabled]} type="checkbox" label="Enabled" /> + <:actions> + <.button>Save Oidc app + + +
    diff --git a/lib/comfycamp_web/controllers/oidc_app_html/show.html.heex b/lib/comfycamp_web/controllers/oidc_app_html/show.html.heex new file mode 100644 index 0000000..88402f1 --- /dev/null +++ b/lib/comfycamp_web/controllers/oidc_app_html/show.html.heex @@ -0,0 +1,20 @@ +
    + <.header> + Oidc app <%= @oidc_app.id %> + <:subtitle>This is a oidc_app record from your database. + <:actions> + <.link href={~p"/admin/oidc_apps/#{@oidc_app}/edit"}> + <.button>Edit oidc_app + + + + + <.list> + <:item title="Name"><%= @oidc_app.name %> + <:item title="Client"><%= @oidc_app.client_id %> + <:item title="Client secret"><%= @oidc_app.client_secret %> + <:item title="Enabled"><%= @oidc_app.enabled %> + + + <.back navigate={~p"/admin/oidc_apps"}>Back to oidc_apps +
    diff --git a/lib/comfycamp_web/router.ex b/lib/comfycamp_web/router.ex index 1275097..f71aba5 100644 --- a/lib/comfycamp_web/router.ex +++ b/lib/comfycamp_web/router.ex @@ -95,6 +95,7 @@ defmodule ComfycampWeb.Router do get "/services", AdminPageController, :services resources "/notes", NotesEditorController resources "/users", UserEditorController, only: [:index, :show] + resources "/oidc_apps", OIDCAppController put "/users/:id/approve", UserEditorController, :approve put "/users/:id/disapprove", UserEditorController, :disapprove end diff --git a/priv/repo/migrations/20240904204356_create_oidc_apps.exs b/priv/repo/migrations/20240904204356_create_oidc_apps.exs new file mode 100644 index 0000000..165ce78 --- /dev/null +++ b/priv/repo/migrations/20240904204356_create_oidc_apps.exs @@ -0,0 +1,14 @@ +defmodule Comfycamp.Repo.Migrations.CreateOidcApps do + use Ecto.Migration + + def change do + create table(:oidc_apps) do + add :name, :string + add :client_id, :string + add :client_secret, :string + add :enabled, :boolean, default: false, null: false + + timestamps(type: :utc_datetime) + end + end +end diff --git a/test/comfycamp/sso_test.exs b/test/comfycamp/sso_test.exs new file mode 100644 index 0000000..73ed684 --- /dev/null +++ b/test/comfycamp/sso_test.exs @@ -0,0 +1,76 @@ +defmodule Comfycamp.SSOTest do + use Comfycamp.DataCase + + alias Comfycamp.SSO + + describe "oidc_apps" do + alias Comfycamp.SSO.OIDCApp + + import Comfycamp.SSOFixtures + + @invalid_attrs %{enabled: nil, name: nil, client_id: nil, client_secret: nil} + + test "list_oidc_apps/0 returns all oidc_apps" do + oidc_app = oidc_app_fixture() + assert SSO.list_oidc_apps() == [oidc_app] + end + + test "get_oidc_app!/1 returns the oidc_app with given id" do + oidc_app = oidc_app_fixture() + assert SSO.get_oidc_app!(oidc_app.id) == oidc_app + end + + test "create_oidc_app/1 with valid data creates a oidc_app" do + valid_attrs = %{ + enabled: true, + name: "some name", + client_id: "some client_id", + client_secret: "some client_secret" + } + + assert {:ok, %OIDCApp{} = oidc_app} = SSO.create_oidc_app(valid_attrs) + assert oidc_app.enabled == true + assert oidc_app.name == "some name" + assert oidc_app.client_id == "some client_id" + assert oidc_app.client_secret == "some client_secret" + end + + test "create_oidc_app/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = SSO.create_oidc_app(@invalid_attrs) + end + + test "update_oidc_app/2 with valid data updates the oidc_app" do + oidc_app = oidc_app_fixture() + + update_attrs = %{ + enabled: false, + name: "some updated name", + client_id: "some updated client_id", + client_secret: "some updated client_secret" + } + + assert {:ok, %OIDCApp{} = oidc_app} = SSO.update_oidc_app(oidc_app, update_attrs) + assert oidc_app.enabled == false + assert oidc_app.name == "some updated name" + assert oidc_app.client_id == "some updated client_id" + assert oidc_app.client_secret == "some updated client_secret" + end + + test "update_oidc_app/2 with invalid data returns error changeset" do + oidc_app = oidc_app_fixture() + assert {:error, %Ecto.Changeset{}} = SSO.update_oidc_app(oidc_app, @invalid_attrs) + assert oidc_app == SSO.get_oidc_app!(oidc_app.id) + end + + test "delete_oidc_app/1 deletes the oidc_app" do + oidc_app = oidc_app_fixture() + assert {:ok, %OIDCApp{}} = SSO.delete_oidc_app(oidc_app) + assert_raise Ecto.NoResultsError, fn -> SSO.get_oidc_app!(oidc_app.id) end + end + + test "change_oidc_app/1 returns a oidc_app changeset" do + oidc_app = oidc_app_fixture() + assert %Ecto.Changeset{} = SSO.change_oidc_app(oidc_app) + end + end +end diff --git a/test/comfycamp_web/controllers/oidc_app_controller_test.exs b/test/comfycamp_web/controllers/oidc_app_controller_test.exs new file mode 100644 index 0000000..17260f0 --- /dev/null +++ b/test/comfycamp_web/controllers/oidc_app_controller_test.exs @@ -0,0 +1,100 @@ +defmodule ComfycampWeb.OIDCAppControllerTest do + use ComfycampWeb.ConnCase + + import Comfycamp.SSOFixtures + + @create_attrs %{ + enabled: true, + name: "some name", + client_id: "some client_id", + client_secret: "some client_secret" + } + @update_attrs %{ + enabled: false, + name: "some updated name", + client_id: "some updated client_id", + client_secret: "some updated client_secret" + } + @invalid_attrs %{enabled: nil, name: nil, client_id: nil, client_secret: nil} + + describe "index" do + setup [:register_and_log_in_admin_user] + + test "lists all oidc_apps", %{conn: conn} do + conn = get(conn, ~p"/admin/oidc_apps") + assert html_response(conn, 200) =~ "Listing Oidc apps" + end + end + + describe "new oidc_app" do + setup [:register_and_log_in_admin_user] + + test "renders form", %{conn: conn} do + conn = get(conn, ~p"/admin/oidc_apps/new") + assert html_response(conn, 200) =~ "New Oidc app" + end + end + + describe "create oidc_app" do + setup [:register_and_log_in_admin_user] + + test "redirects to show when data is valid", %{conn: conn} do + conn = post(conn, ~p"/admin/oidc_apps", oidc_app: @create_attrs) + + assert %{id: id} = redirected_params(conn) + assert redirected_to(conn) == ~p"/admin/oidc_apps/#{id}" + + conn = get(conn, ~p"/admin/oidc_apps/#{id}") + assert html_response(conn, 200) =~ "Oidc app #{id}" + end + + test "renders errors when data is invalid", %{conn: conn} do + conn = post(conn, ~p"/admin/oidc_apps", oidc_app: @invalid_attrs) + assert html_response(conn, 200) =~ "New Oidc app" + end + end + + describe "edit oidc_app" do + setup [:create_oidc_app, :register_and_log_in_admin_user] + + test "renders form for editing chosen oidc_app", %{conn: conn, oidc_app: oidc_app} do + conn = get(conn, ~p"/admin/oidc_apps/#{oidc_app}/edit") + assert html_response(conn, 200) =~ "Edit Oidc app" + end + end + + describe "update oidc_app" do + setup [:create_oidc_app, :register_and_log_in_admin_user] + + test "redirects when data is valid", %{conn: conn, oidc_app: oidc_app} do + conn = put(conn, ~p"/admin/oidc_apps/#{oidc_app}", oidc_app: @update_attrs) + assert redirected_to(conn) == ~p"/admin/oidc_apps/#{oidc_app}" + + conn = get(conn, ~p"/admin/oidc_apps/#{oidc_app}") + assert html_response(conn, 200) =~ "some updated name" + end + + test "renders errors when data is invalid", %{conn: conn, oidc_app: oidc_app} do + conn = put(conn, ~p"/admin/oidc_apps/#{oidc_app}", oidc_app: @invalid_attrs) + assert html_response(conn, 200) =~ "Edit Oidc app" + end + end + + describe "delete oidc_app" do + setup [:create_oidc_app, :register_and_log_in_admin_user] + + test "deletes chosen oidc_app", %{conn: conn, oidc_app: oidc_app} do + conn = delete(conn, ~p"/admin/oidc_apps/#{oidc_app}") + assert redirected_to(conn) == ~p"/admin/oidc_apps" + + assert_error_sent 404, fn -> + get(conn, ~p"/admin/oidc_apps/#{oidc_app}") + end + end + end + + defp create_oidc_app(_) do + oidc_app = oidc_app_fixture() + %{oidc_app: oidc_app} + end +end diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 7a2e9fc..1eb134c 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -15,6 +15,7 @@ defmodule ComfycampWeb.ConnCase do this option is not recommended for other databases. """ + alias Comfycamp.Repo use ExUnit.CaseTemplate using do @@ -49,6 +50,25 @@ defmodule ComfycampWeb.ConnCase do %{conn: log_in_user(conn, user), user: user} end + @doc """ + Setup helper that registers and logs in admin users. + + setup :register_and_log_in_admin_user + + It stores an updated connection and a registered user in the + test context. + """ + def register_and_log_in_admin_user(%{conn: conn}) do + user = Comfycamp.AccountsFixtures.user_fixture(%{is_admin: true}) + + {:ok, user} = + user + |> Comfycamp.Accounts.User.admin_status_changeset(%{is_admin: true}) + |> Repo.update() + + %{conn: log_in_user(conn, user), user: user} + end + @doc """ Logs the given `user` into the `conn`. diff --git a/test/support/fixtures/sso_fixtures.ex b/test/support/fixtures/sso_fixtures.ex new file mode 100644 index 0000000..cd2e5cb --- /dev/null +++ b/test/support/fixtures/sso_fixtures.ex @@ -0,0 +1,23 @@ +defmodule Comfycamp.SSOFixtures do + @moduledoc """ + This module defines test helpers for creating + entities via the `Comfycamp.SSO` context. + """ + + @doc """ + Generate a oidc_app. + """ + def oidc_app_fixture(attrs \\ %{}) do + {:ok, oidc_app} = + attrs + |> Enum.into(%{ + client_id: "some client_id", + client_secret: "some client_secret", + enabled: true, + name: "some name" + }) + |> Comfycamp.SSO.create_oidc_app() + + oidc_app + end +end