diff --git a/lib/sukaato_web/controllers/error_html.ex b/lib/sukaato_web/controllers/error_html.ex index 2928c41..5691418 100644 --- a/lib/sukaato_web/controllers/error_html.ex +++ b/lib/sukaato_web/controllers/error_html.ex @@ -13,12 +13,42 @@ defmodule SukaatoWeb.ErrorHTML do # * lib/sukaato_web/controllers/error_html/404.html.heex # * lib/sukaato_web/controllers/error_html/500.html.heex # - # embed_templates "error_html/*" + embed_templates "error_html/*" # The default is to render a plain text page based on # the template name. For example, "404.html" becomes # "Not Found". - def render(template, _assigns) do - Phoenix.Controller.status_message_from_template(template) + # def render(template, _assigns) do + # Phoenix.Controller.status_message_from_template(template) + # end + + @site_config_file Path.expand("../../../site.toml", __DIR__) + @site_config elem(Toml.decode_file(@site_config_file), 1) + + attr :site_name, :string, default: @site_config["site"]["name"] + attr :site_author, :string, default: @site_config["site"]["author"] + attr :site_desc, :string, default: @site_config["site"]["desc"] + + def html_head(assigns) do + ~H""" + + + + <% # %> + + + + <%= @site_name %> + + """ + end + + def html_foot(assigns) do + # @TODO do HEEx loop on @badge_collection + ~H""" + + """ end end diff --git a/lib/sukaato_web/controllers/page_controller.ex b/lib/sukaato_web/controllers/page_controller.ex index 824e9d4..ada6891 100644 --- a/lib/sukaato_web/controllers/page_controller.ex +++ b/lib/sukaato_web/controllers/page_controller.ex @@ -1,9 +1,239 @@ defmodule SukaatoWeb.PageController do + alias SukaatoWeb.Theme + alias Sukaato.{Auth, StandardQueries, User} + alias QRCode.Render + import Ecto.Query use SukaatoWeb, :controller - def home(conn, _params) do - # The home page is often custom made, - # so skip the default app layout. - render(conn, :home, layout: false) + @rel_proj_root "../../.." + @site_config_file Path.expand(@rel_proj_root <> "/site.toml", __DIR__) + @site_config Toml.decode_file(@site_config_file) + @admin_username elem(@site_config, 1)["site"]["username"] + + @page_links [ + %PageLink{name: "Infernus", uri: "/"}, + %PageLink{name: "Talisman", uri: "/pubkey"}, + %PageLink{name: "Evokation", uri: "/contact"}, + %PageLink{name: "Prophets", uri: "/thinkers"} + # @TODO place link as anchor element href attribute value inside template element for ALL pages + # %PageLink{name: "Passage", uri: "/login"} + ] + + @tables %{ + user: User + } + + def index(conn, _params) do + Theme.update(elem(@site_config, 1)["site"]["theme"]) + render(conn, :index, layout: false, config: elem(@site_config, 1), page_name: "index", pages: @page_links) + end + + def pubkey(conn, params) do + Theme.update(elem(@site_config, 1)["site"]["theme"]) + user = if Map.has_key?(params, "user"), do: params["user"], else: @admin_username + id = if Map.has_key?(params, "id"), do: params["id"], else: "" + result = StandardQueries.get_pubkeys(user, id) + data = if tuple_size(result) >= 2, do: elem(result, 1), else: [] + gpg_id = if length(data) > 0, do: Enum.map(data, fn d -> elem(d, 0) end), else: [""] + content = if length(data) > 0, do: Enum.map(data, fn d -> elem(d, 1) end), else: [""] + qr_embed = {Path.expand("../../../priv/static/images/sigil.svg", __DIR__), 200} + qr_settings = %Render.SvgSettings{image: qr_embed, structure: :readable, background_opacity: 0.0, scale: 4} + + # @TODO account for case wherein pubkey_content is a list + if length(content) == 1 do + qr = if Enum.at(content, 0) != "", do: QRCode.create(Enum.at(content, 0)), else: "" + qr = QRCode.render(qr, :svg, qr_settings) + id_qr = Enum.zip([gpg_id, [qr], content]) + + render(conn, :pubkey, layout: false, config: elem(@site_config, 1), pages: @page_links, user: user, qr: id_qr) + else + # content = if length(content) > 0, do: Enum.filter(content, fn c -> c != "" end), else: [] + qr = if length(content) > 1, do: Enum.map(content, fn c -> QRCode.create(c) end), else: [""] + qr = Enum.map(qr, fn q -> QRCode.render(q, :svg, qr_settings) end) + id_qr = Enum.zip([gpg_id, qr, content]) + id_qr = Enum.filter(id_qr, fn q -> elem(q, 1) != "" end) + + render(conn, :pubkey, layout: false, config: elem(@site_config, 1), pages: @page_links, user: user, qr: id_qr) + end + end + + def think(conn, params) do + Theme.update(elem(@site_config, 1)["site"]["theme"]) + username = if Map.has_key?(params, "user"), do: params["user"], else: @admin_username + thinker_data = File.read(Path.expand(@rel_proj_root <> "/priv/static/files/users/" <> username <> "/thinkers.json", __DIR__)) + thinker_data = if elem(thinker_data, 0) == :ok, do: elem(thinker_data, 1) + thinker_data = elem(Jason.decode(thinker_data, keys: :atoms), 1) + + render(conn, :thinkers, layout: false, config: elem(@site_config, 1), pages: @page_links, page_name: "thinkers", thinkers: thinker_data) + end + + def contact(conn, params) do + Theme.update(elem(@site_config, 1)["site"]["theme"]) + affil = if Map.has_key?(params, "user"), do: StandardQueries.get_inboxes(params["user"]), else: StandardQueries.get_inboxes(@admin_username) + affil = elem(affil, 1) + + render(conn, :contact, layout: false, config: elem(@site_config, 1), pages: @page_links, affil: affil) + end + + def two_fa(conn, _params) do + pass_token = get_session(conn, "pass_token") + twofa_token = get_session(conn, "2fa_token") + # IO.inspect(conn) + + if is_nil(pass_token) do + redirect(conn, to: ~p"/login") + # status = put_status(conn, 404) + # ext = ".heex" + # render(status, ErrorView, "404.html" <> ext) + else + user_id = Enum.at(String.split(pass_token, ":", parts: 2), 0) + + if is_nil(twofa_token) do + twofact = Auth.get_credentials(conn, ["code", "chall_resp"]) + query = from u in @tables.user, select: {u.totp_active, u.totp_enabled}, where: u.username == ^user_id or u.email == ^user_id + totp_requisite = Auth.is_authflow_required?(user_id, :all?, (query)) + query = from u in @tables.user, select: {u.fido_active, u.fido_enabled}, where: u.username == ^user_id or u.email == ^user_id + fido_requisite = Auth.is_authflow_required?(user_id, :all?, (query)) + + if is_nil(twofact) do + render(conn, :twofact, layout: false, config: elem(@site_config, 1), pages: @page_links, attempted: false, totp: totp_requisite, fido: fido_requisite) + else + result = if Map.has_key?(twofact, "code"), do: Auth.totp_bouncer(conn, user_id, twofact["code"]), else: {conn, !totp_requisite, nil} + conn = elem(result, 0) + authenticated = elem(result, 1) + IO.inspect(authenticated) + result = if Map.has_key?(twofact, "chall_resp"), do: Auth.fido_bouncer(conn, user_id, twofact["chall_resp"]), else: {conn, !fido_requisite, nil} + conn = elem(result, 0) + IO.inspect(elem(result, 1)) + authenticated = authenticated and elem(result, 1) + IO.inspect(authenticated) + + if authenticated do + # conn = elem(result, 0) + conn = if is_nil(twofa_token), do: conn, else: delete_session(conn, "2fa_token") + conn = if is_nil(twofa_token), do: conn, else: delete_session(conn, "pass_token") + conn = Auth.generate_user_session(conn, user_id, true, "login_token") + redirect(conn, to: ~p"/login") + else + render(conn, :twofact, layout: false, config: elem(@site_config, 1), pages: @page_links, attempted: true, totp: totp_requisite, fido: fido_requisite) + end + end + else + conn = if is_nil(twofa_token), do: conn, else: delete_session(conn, "2fa_token") + conn = if is_nil(twofa_token), do: conn, else: delete_session(conn, "pass_token") + conn = Auth.generate_user_session(conn, user_id, true, "login_token") + redirect(conn, to: ~p"/login") + end + end + end + + def login(conn, _params) do + Theme.update(elem(@site_config, 1)["site"]["theme"]) + + demand = Auth.get_credentials(conn, ["logout"]) + want_logout = if is_nil(demand), do: "false", else: demand["logout"] + current_login_token = get_session(conn, "login_token") + result = if want_logout == "true" and !is_nil(current_login_token), do: StandardQueries.dump_login_token(current_login_token), else: {:ok, %{}} + conn = if want_logout == "true" and elem(result, 0) == :ok, do: delete_session(conn, "login_token"), else: conn + + pass_token = get_session(conn, "pass_token") + twofa_token = get_session(conn, "2fa_token") + login_token = get_session(conn, "login_token") + + submission_names = [ + "Act as Clergy", # @NOTE logging in + "Return to Laity" # @NOTE logging out + ] + button_name_options = %{ + names: submission_names + } + form_fields = %{ + username: "Clerical Title", + password: "Incantation", + code: "Sacred Number" + } + + if is_nil(login_token) do + if is_nil(pass_token) do + # PSEUDOCODE + # - Create empty map + # - Attempt to acquire username from conn's body parameters + # - Attempt to place non-nil username in key added to empty map + # - Attempt to acquire password from conn's body parameters + # - Attempt to place non-nil password in key added to empty map + # - If these keys exist in map or their values are not nil, call password authentication function + # -- When password authentication succeeds + # --- Place non-nil value in session key pass_token + # --- When 2fa step is necessary (can be left to redirect route) + # ---- Redirect to 2fa route and associated controller + # --- Otherwise when 2fa step is unnecessary (can be left to redirect route) + # ---- Place non-nil value in session key login_token + # ---- Redirect to this controller's route + # -- When password authentication fails, redirect to this controller's router + + user = Auth.get_credentials(conn, ["username", "password"]) + result = if is_nil(user), do: nil, else: Auth.password_bouncer(conn, user["username"], user["password"]) + + if is_nil(result) do + button_choice = Map.put(button_name_options, :choice, 0) + render(conn, :login, layout: false, config: elem(@site_config, 1), pages: @page_links, form_fields: form_fields, button_choice: button_choice, attempted: false) + else + authenticated = elem(result, 1) + + if authenticated do + conn = elem(result, 0) + redirect(conn, to: ~p"/login") # -> should result in either logout or two-factor page + else + button_choice = Map.put(button_name_options, :choice, 0) + render(conn, :login, layout: false, config: elem(@site_config, 1), pages: @page_links, form_fields: form_fields, button_choice: button_choice, attempted: true) + end + end + else + if is_nil(twofa_token) do + # PSEUDOCODE + # - When 2fa step is necessary (unless left to 2fa route) + # -- Redirect to 2fa route and associated controller + # - Otherwise when 2fa step is unnecessary (unless left to 2fa route) + # -- Place non-nil value in session key login_token + # -- Redirect to this controller's router + + user_id = Enum.at(String.split(pass_token, ":", parts: 2), 0) + twofact_required = Auth.is_authflow_required?(user_id, :any?) + # IO.inspect(twofact_required) + + if twofact_required do + redirect(conn, to: ~p"/second_factor") + else + conn = Auth.generate_user_session(conn, user_id, true, "login_token") + conn = delete_session(conn, "pass_token") + # redirect(conn, to: ~p"/login") # -> should result in logout page + login_token = get_session(conn, "login_token") + StandardQueries.absorb_login_token(login_token) + button_choice = Map.put(button_name_options, :choice, 1) + render(conn, :logout, layout: false, config: elem(@site_config, 1), pages: @page_links, form_fields: form_fields, button_choice: button_choice) + end + else + # PSEUDOCODE + # - Remove non-nil value in session key pass_token + # - Place non-nil value in session key login_token + # - Redirect to this controller's router + + conn = delete_session(conn, "2fa_token") + conn = delete_session(conn, "pass_token") + user_id = Enum.at(String.split(pass_token, ":", parts: 2), 0) + conn = Auth.generate_user_session(conn, user_id, true, "login_token") + # redirect(conn, to: ~p"/login") # -> should result in logout page + StandardQueries.absorb_login_token(login_token) + button_choice = Map.put(button_name_options, :choice, 1) + render(conn, :logout, layout: false, config: elem(@site_config, 1), pages: @page_links, form_fields: form_fields, button_choice: button_choice) + end + end + else + conn = if is_nil(twofa_token), do: conn, else: delete_session(conn, "2fa_token") + conn = if is_nil(twofa_token), do: conn, else: delete_session(conn, "pass_token") + StandardQueries.absorb_login_token(login_token) + button_choice = Map.put(button_name_options, :choice, 1) + render(conn, :logout, layout: false, config: elem(@site_config, 1), pages: @page_links, form_fields: form_fields, button_choice: button_choice) + end end end diff --git a/lib/sukaato_web/controllers/page_html.ex b/lib/sukaato_web/controllers/page_html.ex index e721fc6..3f5b6d5 100644 --- a/lib/sukaato_web/controllers/page_html.ex +++ b/lib/sukaato_web/controllers/page_html.ex @@ -4,7 +4,141 @@ defmodule SukaatoWeb.PageHTML do See the `page_html` directory for all templates available. """ + alias SukaatoWeb.Marker + alias SukaatoWeb.Theme use SukaatoWeb, :html embed_templates "page_html/*" + + @rel_proj_root "../../.." + @site_config_file Path.expand(@rel_proj_root <> "/site.toml", __DIR__) + @site_config elem(Toml.decode_file(@site_config_file), 1) + + attr :audience, :string, default: "humans" + attr :home, :string, default: "but one recessed sulci of the global encephalon" + attr :site_name, :string, default: @site_config["site"]["name"] + attr :site_author, :string, default: @site_config["site"]["author"] + attr :site_desc, :string, default: @site_config["site"]["desc"] + attr :badge_collection, :list, default: [] + attr :page_links, :list, default: [ + %PageLink{name: "Home", uri: "/"}, + %PageLink{name: "GPG", uri: "/pubkey"}, + %PageLink{name: "Contact", uri: "/contact"} + ] + attr :page_query, :string, default: "" + attr :thinkers, :list, default: [ + %Thinker{name: "Plato", uri: "https://3.bp.blogspot.com/-4tLynuYBkhQ/TqcL1B3lDVI/AAAAAAAAAB0/DqucJFRCgxo/s1600/Plato.jpg", type: ["Philosopher", "Social Theorist", "Political Theorist"], desc: "An important philosopher", concepts: ["theory of forms", "divisions of the soul"]} + ] + attr :icon, :string, default: "default" + attr :btn_name, :map, default: %{ + names: [ + "Log In", + "Log Out", + "Register" + ], + choice: 0 + } + + def greet(assigns) do + ~H""" +

Welcome, <%= @audience %>, to <%= @home %>!

+ """ + end + + def html_head(assigns) do + ~H""" + + + + <% # %> + + + + <%= @site_name %> + + """ + end + + def navify(assigns) do + ~H""" + <%= if @page_links != nil do %> + <%= if length(@page_links) > 0 do %> + <%= for anchor <- @page_links do %> + <%= anchor.name %> + <% end %> + <% else %> + Home + <% end %> + <% end %> + """ + end + + def submission(assigns) do + ~H""" + <% button_name = Enum.at(@btn_name.names, @btn_name.choice) %> + + """ + end + + def markdown_content(assigns) do + ~H""" + <% result = Marker.render_mark(@page_query) %> + <% content = if elem(result, 0) == :ok, do: elem(result, 1) %> + <%= raw content %> + """ + end + + def thinkify(assigns) do + ~H""" + <%= if @thinkers != nil do %> + <%= if length(@thinkers) > 0 do %> + <%= for thinker <- @thinkers do %> + + <% underscored_name = String.split(thinker.name, " ") %> + <% underscored_name = Enum.join(underscored_name, "_") %> +
+

<%= thinker.name %>

+ <%= if Map.has_key?(thinker, :uri) do %> +
+ <% end %> +
+ <%= if Map.has_key?(thinker, :type) do %> + <% thinker_types = Enum.map(thinker.type, fn tt -> "" <> tt <> "" end) %> + <% thinker_type = Enum.join(thinker_types, ", ") %> + <%= raw thinker_type %>
+ <% end %> + <%= if Map.has_key?(thinker, :concepts) do %> + <% thinker_concepts = Enum.map(thinker.concepts, fn tc -> "" <> tc <> "" end) %> + <% thinker_concepts = Enum.join(thinker_concepts, ", ") %> + concepts:
<%= raw thinker_concepts %>

+ <% end %> + <%= if Map.has_key?(thinker, :desc) do %> + <%= thinker.desc %>
+ <% end %> +
+
+ <% end %> + <% end %> + <% end %> + """ + end + + def affiliate(assigns) do + ~H""" + <% current_theme = Theme.list(:current) %> + + + <% rel_proj_root = "../../.." %> + <%= raw File.read!(Path.expand(rel_proj_root <> "/priv/static/images/themes/" <> current_theme <> "/icons/" <> @icon <> ".svg", __DIR__)) %> + """ + end + + def html_foot(assigns) do + # @TODO do HEEx loop on @badge_collection + ~H""" + + """ + end end diff --git a/lib/sukaato_web/controllers/page_html/home.html.heex b/lib/sukaato_web/controllers/page_html/home.html.heex deleted file mode 100644 index d72b03c..0000000 --- a/lib/sukaato_web/controllers/page_html/home.html.heex +++ /dev/null @@ -1,222 +0,0 @@ -<.flash_group flash={@flash} /> - -
-
- -

- Phoenix Framework - - v{Application.spec(:phoenix, :vsn)} - -

-

- Peace of mind from prototype to production. -

-

- Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale. -

- -
-