From 2407fa8b4343c2111ec6aa8ba2fcec015f77ebf8 Mon Sep 17 00:00:00 2001 From: emielvdveen Date: Tue, 21 Nov 2023 12:48:22 +0100 Subject: [PATCH] Publishing of assignment --- core/bundles/next/lib/menu/items.ex | 2 +- core/lib/core/accounts.ex | 19 ++++++ core/lib/core_web/controllers/user_auth.ex | 9 +++ core/lib/core_web/live/menu/builder.ex | 2 +- core/lib/external_sign_in.ex | 59 +++++++++++++++++++ core/lib/external_sign_in/user.ex | 21 +++++++ .../gettext/en/LC_MESSAGES/eyra-assignment.po | 2 +- .../gettext/nl/LC_MESSAGES/eyra-assignment.po | 2 +- .../20231120165201_add_external_user.exs | 20 +++++++ core/systems/assignment/_public.ex | 7 +++ .../assignment/content_page_builder.ex | 13 ++-- core/systems/assignment/crew_page_builder.ex | 2 +- .../assignment/external_panel_controller.ex | 50 +++++++++++++++- 13 files changed, 198 insertions(+), 10 deletions(-) create mode 100644 core/lib/external_sign_in.ex create mode 100644 core/lib/external_sign_in/user.ex create mode 100644 core/priv/repo/migrations/20231120165201_add_external_user.exs diff --git a/core/bundles/next/lib/menu/items.ex b/core/bundles/next/lib/menu/items.ex index f8c410eae..ba9b88450 100644 --- a/core/bundles/next/lib/menu/items.ex +++ b/core/bundles/next/lib/menu/items.ex @@ -7,7 +7,7 @@ defmodule Next.Menu.Items do @impl true def values() do %{ - next: %{action: %{type: :http_get, to: ~p"/next"}, title: "Next"}, + next: %{action: %{type: :http_get, to: ~p"/"}, title: "Next"}, admin: %{ action: %{type: :redirect, to: ~p"/admin/config"}, title: dgettext("eyra-ui", "menu.item.admin") diff --git a/core/lib/core/accounts.ex b/core/lib/core/accounts.ex index bc7cc94f5..76aef21aa 100644 --- a/core/lib/core/accounts.ex +++ b/core/lib/core/accounts.ex @@ -40,6 +40,25 @@ defmodule Core.Accounts do |> Repo.all() end + ## External + + def external?(%User{id: user_id}) do + external?(user_id) + end + + def external?(user_id) do + from(ex in ExternalSignIn.User, + where: ex.user_id == ^user_id + ) + |> Repo.exists?() + end + + def internal?(nil), do: false + + def internal?(user) do + not external?(user) + end + ## Search def search(query, preload \\ []) do diff --git a/core/lib/core_web/controllers/user_auth.ex b/core/lib/core_web/controllers/user_auth.ex index 1966d66ab..a1aa214a8 100644 --- a/core/lib/core_web/controllers/user_auth.ex +++ b/core/lib/core_web/controllers/user_auth.ex @@ -39,6 +39,15 @@ defmodule CoreWeb.UserAuth do |> redirect(to: redirect_to) end + def log_in_user_without_redirect(conn, user) do + token = Accounts.generate_user_session_token(user) + + conn + |> renew_session() + |> put_session(:user_token, token) + |> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}") + end + defp maybe_write_remember_me_cookie(conn, token, %{"remember_me" => "true"}) do put_resp_cookie(conn, @remember_me_cookie, token, @remember_me_options) end diff --git a/core/lib/core_web/live/menu/builder.ex b/core/lib/core_web/live/menu/builder.ex index 1c8f40ed6..59e62a8f2 100644 --- a/core/lib/core_web/live/menu/builder.ex +++ b/core/lib/core_web/live/menu/builder.ex @@ -16,7 +16,7 @@ defmodule CoreWeb.Menu.Builder do admin: Admin.Public.admin?(user), support: Admin.Public.admin?(user), debug: Admin.Public.admin?(user), - profile: not is_nil(user), + profile: Core.Accounts.internal?(user), signout: not is_nil(user), signin: is_nil(user) } diff --git a/core/lib/external_sign_in.ex b/core/lib/external_sign_in.ex new file mode 100644 index 000000000..d80ee74b3 --- /dev/null +++ b/core/lib/external_sign_in.ex @@ -0,0 +1,59 @@ +defmodule ExternalSignIn do + alias Core.Accounts + alias Core.Repo + import Ecto.Query, warn: false + + def sign_in(conn, organisation, external_id) do + user = + if user = get_user_by_external_id(external_id) do + user + else + register_user(organisation, external_id) + end + + CoreWeb.UserAuth.log_in_user_without_redirect(conn, user) + end + + def get_user_by_external_id(external_id) do + external_user_query = + from(ex in ExternalSignIn.User, + where: ex.external_id == ^external_id, + select: ex.user_id + ) + + from(u in Accounts.User, where: u.id in subquery(external_user_query)) + |> Repo.one() + end + + def register_user(organisation, external_id) when is_atom(organisation) do + register_user(Atom.to_string(organisation), external_id) + end + + def register_user(organisation, external_id) do + name = "#{organisation}_#{external_id}" + + user = + Accounts.User.sso_changeset(%Accounts.User{}, %{ + email: "external_#{name}@eyra.co", + researcher: false, + student: false, + displayname: name, + profile: %{ + fullname: name + } + }) + + external_user = + ExternalSignIn.User.changeset(%ExternalSignIn.User{}, %{ + external_id: external_id, + organisation: organisation + }) + + {:ok, result} = + external_user + |> Ecto.Changeset.put_assoc(:user, user) + |> Repo.insert() + + result.user + end +end diff --git a/core/lib/external_sign_in/user.ex b/core/lib/external_sign_in/user.ex new file mode 100644 index 000000000..45fef40cf --- /dev/null +++ b/core/lib/external_sign_in/user.ex @@ -0,0 +1,21 @@ +defmodule ExternalSignIn.User do + use Ecto.Schema + import Ecto.Changeset + + @fields ~w(external_id organisation)a + @required_fields @fields + + schema "external_users" do + belongs_to(:user, Core.Accounts.User) + field(:external_id, :string) + field(:organisation, :string) + + timestamps() + end + + def changeset(%ExternalSignIn.User{} = user, attrs) do + user + |> cast(attrs, @fields) + |> validate_required(@required_fields) + end +end diff --git a/core/priv/gettext/en/LC_MESSAGES/eyra-assignment.po b/core/priv/gettext/en/LC_MESSAGES/eyra-assignment.po index f00fd7731..ffe214ff5 100644 --- a/core/priv/gettext/en/LC_MESSAGES/eyra-assignment.po +++ b/core/priv/gettext/en/LC_MESSAGES/eyra-assignment.po @@ -76,7 +76,7 @@ msgstr "Publish" #, elixir-autogen, elixir-format, fuzzy msgid "retract.button" -msgstr "Contact researcher" +msgstr "Take offline" #, elixir-autogen, elixir-format msgid "onboarding.consent.continue.button" diff --git a/core/priv/gettext/nl/LC_MESSAGES/eyra-assignment.po b/core/priv/gettext/nl/LC_MESSAGES/eyra-assignment.po index 2cf3cb428..06cd04327 100644 --- a/core/priv/gettext/nl/LC_MESSAGES/eyra-assignment.po +++ b/core/priv/gettext/nl/LC_MESSAGES/eyra-assignment.po @@ -76,7 +76,7 @@ msgstr "Publiceren" #, elixir-autogen, elixir-format, fuzzy msgid "retract.button" -msgstr "Intrekken" +msgstr "Offline halen" #, elixir-autogen, elixir-format msgid "onboarding.consent.continue.button" diff --git a/core/priv/repo/migrations/20231120165201_add_external_user.exs b/core/priv/repo/migrations/20231120165201_add_external_user.exs new file mode 100644 index 000000000..73aa4a029 --- /dev/null +++ b/core/priv/repo/migrations/20231120165201_add_external_user.exs @@ -0,0 +1,20 @@ +defmodule Core.Repo.Migrations.AddExternalUser do + use Ecto.Migration + + def up do + create table(:external_users) do + add(:user_id, references(:users, on_delete: :delete_all), null: false) + add(:organisation, :string) + add(:external_id, :string) + + timestamps() + end + + create(unique_index(:external_users, [:organisation, :external_id])) + end + + def down do + drop(index(:external_users, [:organisation, :external_id])) + drop(table(:external_users)) + end +end diff --git a/core/systems/assignment/_public.ex b/core/systems/assignment/_public.ex index 67fc12cf4..1bbde7c3f 100644 --- a/core/systems/assignment/_public.ex +++ b/core/systems/assignment/_public.ex @@ -224,6 +224,13 @@ defmodule Systems.Assignment.Public do Core.Persister.save(assignment, changeset) end + def update!(assignment, %{} = attrs) do + case __MODULE__.update(assignment, attrs) do + {:ok, assignment} -> assignment + _ -> nil + end + end + def update_budget(assignment, budget) do changeset = Assignment.Model.changeset(assignment, %{}) diff --git a/core/systems/assignment/content_page_builder.ex b/core/systems/assignment/content_page_builder.ex index 36df51be8..e1ed82102 100644 --- a/core/systems/assignment/content_page_builder.ex +++ b/core/systems/assignment/content_page_builder.ex @@ -133,19 +133,24 @@ defmodule Systems.Assignment.ContentPageBuilder do do: [publish: publish, preview: preview] defp handle_publish(socket) do - socket + socket |> set_status(:online) end defp handle_retract(socket) do - socket + socket |> set_status(:offline) end defp handle_close(socket) do - socket + socket |> set_status(:idle) end defp handle_open(socket) do - socket + socket |> set_status(:concept) + end + + defp set_status(%{assigns: %{model: assignment}} = socket, status) do + assignment = Assignment.Public.update!(assignment, %{status: status}) + socket |> Phoenix.Component.assign(model: assignment) end defp create_tabs(assignment, show_errors, assigns) do diff --git a/core/systems/assignment/crew_page_builder.ex b/core/systems/assignment/crew_page_builder.ex index 88a0e87b4..a1d79b86e 100644 --- a/core/systems/assignment/crew_page_builder.ex +++ b/core/systems/assignment/crew_page_builder.ex @@ -42,7 +42,7 @@ defmodule Systems.Assignment.CrewPageBuilder do defp consent_view(%{consent_agreement: nil}, _), do: nil defp consent_view(%{consent_agreement: consent_agreement}, %{current_user: user, fabric: fabric}) do - revision = Consent.Public.latest_revision(consent_agreement) + revision = Consent.Public.latest_revision(consent_agreement, [:signatures]) Fabric.prepare_child(fabric, :onboarding_view, Assignment.OnboardingConsentView, %{ revision: revision, diff --git a/core/systems/assignment/external_panel_controller.ex b/core/systems/assignment/external_panel_controller.ex index cc0c57e36..c89b66d4a 100644 --- a/core/systems/assignment/external_panel_controller.ex +++ b/core/systems/assignment/external_panel_controller.ex @@ -1,12 +1,60 @@ defmodule Systems.Assignment.ExternalPanelController do use CoreWeb, :controller - def create(conn, %{"id" => _id, "panel" => _panel} = params) do + alias Systems.Assignment + alias Systems.Crew + + def create(conn, %{"id" => id, "panel" => _} = params) do + assignment = Assignment.Public.get!(id, [:crew, :auth_node]) + + cond do + has_no_access?(assignment, params) -> forbidden(conn) + is_offline?(assignment) -> service_unavailable(conn) + true -> start(assignment, conn, params) + end + end + + defp is_offline?(%{status: status}) do + status != :online + end + + defp has_no_access?(%{external_panel: external_panel}, %{"panel" => panel}) do + external_panel = Atom.to_string(external_panel) + external_panel != panel + end + + defp start(%{external_panel: panel} = assignment, conn, params) do + participant_id = get_participant(params) + conn + |> ExternalSignIn.sign_in(panel, participant_id) + |> authorize_user(assignment) |> add_panel_info(params) |> redirect(to: path(params)) end + defp forbidden(conn) do + conn + |> put_status(:forbidden) + |> put_view(html: CoreWeb.ErrorHTML) + |> render(:"403") + end + + defp service_unavailable(conn) do + conn + |> put_status(:service_unavailable) + |> put_view(html: CoreWeb.ErrorHTML) + |> render(:"503") + end + + defp authorize_user(%{assigns: %{current_user: user}} = conn, %{crew: crew}) do + if not Crew.Public.member?(crew, user) do + Crew.Public.apply_member_with_role(crew, user, :participant) + end + + conn + end + defp add_panel_info(conn, params) do panel_info = %{ participant: get_participant(params),