diff --git a/assets/js/app.js b/assets/js/app.js index a964a90..5fc04c8 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -14,15 +14,17 @@ import "../css/app.css" // import {Socket} from "phoenix" // import socket from "./socket" // -import "phoenix_html" -import {Socket} from "phoenix" -import topbar from "topbar" -import {LiveSocket} from "phoenix_live_view" +import "phoenix_html"; +import {Socket} from "phoenix"; +import topbar from "topbar"; +import {LiveSocket} from "phoenix_live_view"; import SplashPage from './pages/SplashPage'; -import JustPage from './pages/JustPage' -import ForPage from './pages/ForPage' -import YouPage from './pages/YouPage' +import JustPage from './pages/JustPage'; +import ForPage from './pages/ForPage'; +import YouPage from './pages/YouPage'; +import AuthPage from './pages/AuthPage'; +import RevealPage from './pages/RevealPage'; let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}}) @@ -42,5 +44,5 @@ liveSocket.connect() window.liveSocket = liveSocket window.Components = { - SplashPage, JustPage, ForPage, YouPage + SplashPage, JustPage, ForPage, YouPage, AuthPage, RevealPage } diff --git a/assets/js/pages/AuthPage.tsx b/assets/js/pages/AuthPage.tsx index 97013f8..4215761 100644 --- a/assets/js/pages/AuthPage.tsx +++ b/assets/js/pages/AuthPage.tsx @@ -1,16 +1,54 @@ -// import HexMix from "../utils/hexmix"; -// const fragmentData = window.location.hash.split('.'); -// if (fragmentData.length <= 0) { -// alert("No key found in fragment URI"); -// return; -// } -// const key = HexMix.hexToUint8(fragmentData[0]); -// const iv = HexMix.hexToUint8(fragmentData[1]); +import { Button, CenteredContainer, GlobalStyle, Header2, Header3, Input, Label, Spacer, TextAlignWrapper } from "@intended/intended-ui"; +import React, { useEffect } from "react"; -// const importedKey = await window.crypto.subtle.importKey( -// 'raw', -// key, -// 'AES-GCM', -// true, -// ['encrypt', 'decrypt'] -// ); \ No newline at end of file +type AuthPageProps = { + csrf: string, + service: string, + recipient: string +} + +const AuthPage = (props: AuthPageProps) => { + const { service, recipient } = props; + // const [recipientInput, setRecipientInput] = useState(""); + // const [serviceSelect, setServiceSelect] = useState("github"); + + // useEffect(() => { + + // }, []) + + return ( + + + + + Someone sent you a secret + + Please verify your identity to reveal this message. + + + + + + + + + + + + + + + + + + + ); +} + +export default AuthPage; diff --git a/assets/js/pages/ForPage.tsx b/assets/js/pages/ForPage.tsx index 4e1e4e2..852cee7 100644 --- a/assets/js/pages/ForPage.tsx +++ b/assets/js/pages/ForPage.tsx @@ -3,9 +3,13 @@ import React, { useState } from "react"; import { ProgressIndicator, Header2, Button, IconArrow, Label, Input, Select, CenteredContainer, SpaceBetweenContainer, Spacer, TextAlignWrapper, GlobalStyle } from "@intended/intended-ui"; -const ForPage = () => { +type ForPageProps = { + csrf: string +} + +const ForPage = (props: ForPageProps) => { const [recipientInput, setRecipientInput] = useState(""); - const [serviceSelect, setServiceSelect] = useState(""); + const [serviceSelect, setServiceSelect] = useState("github"); const handleRecipientInputChange = ( e: React.ChangeEvent @@ -18,14 +22,14 @@ const ForPage = () => { }; const postContacts = async () => { - const fragmentData = window.location.hash.split('.'); - if (fragmentData.length <= 0) { - alert("No key found in fragment URI"); - return; - } + // const fragmentData = window.location.hash.split('.'); + // if (fragmentData.length <= 0) { + // alert("No key found in fragment URI"); + // return; + // } const linkId = sessionStorage.getItem("link_id"); - if (linkId == null || linkId == "") { + if (!linkId) { alert("No created link found in storage"); return; } @@ -36,10 +40,18 @@ const ForPage = () => { formData.append("link_id", linkId); try { - await fetch(`${window.location.origin}/just/for`, { + const results = await fetch(`${window.location.origin}/just/for`, { + headers: { + "X-CSRF-Token": props.csrf + }, body: formData, method: "POST" }); + if (!results.ok) { + throw new Error('Network response was not OK'); + } + + await results.json(); window.location.href = `${window.location.origin}/just/for/you`; } catch (err: any) { alert(err.message); @@ -74,7 +86,9 @@ const ForPage = () => { id="serviceSelector" onChange={handleServiceChange} value={serviceSelect} - /> + > + + + + + + ); +} + +export default RevealPage; diff --git a/assets/js/pages/YouPage.tsx b/assets/js/pages/YouPage.tsx index b503757..daa2a80 100644 --- a/assets/js/pages/YouPage.tsx +++ b/assets/js/pages/YouPage.tsx @@ -8,11 +8,10 @@ const YouPage = () => { const keyHex = sessionStorage.getItem("key_hex"); const ivHex = sessionStorage.getItem("iv_hex"); - `${window.location.origin}/just/for/you/${linkId}#${keyHex}.${ivHex}` - return ""; + return `${window.location.origin}/just/for/you/${linkId}#${keyHex}.${ivHex}`; }; - const [url, setUrl] = useState(calculateUrl()); + const [url, _setUrl] = useState(calculateUrl()); const copyUrl = async () => { try { @@ -51,7 +50,7 @@ const YouPage = () => { diff --git a/assets/package-lock.json b/assets/package-lock.json index e89aa39..12dd5f7 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -1079,6 +1079,7 @@ "version": "7.16.3", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz", "integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==", + "dev": true, "requires": { "regenerator-runtime": "^0.13.4" } @@ -1142,9 +1143,9 @@ "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" }, "@intended/intended-ui": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@intended/intended-ui/-/intended-ui-0.1.19.tgz", - "integrity": "sha512-dOQZBvBm5UyKhyilTE6y/3KL1suyjbAuu+kn4b3tKhpy/Y23AglVlHLj4oi/AUgGRMtcRT/o9eySJWkNWQ9weA==", + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/@intended/intended-ui/-/intended-ui-0.1.21.tgz", + "integrity": "sha512-rNyJOGLOw8iKP0AaSYSytZXFssPhlE1FbUjxEze8F4iOgXxs6iu7B9+gSIF4QJg/IrQUVu76tkevBGDk2nTQNg==", "requires": { "polished": "^4.1.3", "react": "^17.0.2", @@ -1719,9 +1720,9 @@ } }, "babel-plugin-styled-components": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.1.tgz", - "integrity": "sha512-U3wmORxerYBiqcRCo6thItIosEIga3F+ph0jJPkiOZJjyhpZyUZFQV9XvrZ2CbBIihJ3rDBC/itQ+Wx3VHMauw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.2.tgz", + "integrity": "sha512-7eG5NE8rChnNTDxa6LQfynwgHTVOYYaHJbUYSlOhk8QBXIQiMBKq4gyfHBBKPrxUcVBXVJL61ihduCpCQbuNbw==", "requires": { "@babel/helper-annotate-as-pure": "^7.16.0", "@babel/helper-module-imports": "^7.16.0", @@ -5896,11 +5897,21 @@ } }, "polished": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/polished/-/polished-4.1.3.tgz", - "integrity": "sha512-ocPAcVBUOryJEKe0z2KLd1l9EBa1r5mSwlKpExmrLzsnIzJo4axsoU9O2BjOTkDGDT4mZ0WFE5XKTlR3nLnZOA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/polished/-/polished-4.1.4.tgz", + "integrity": "sha512-Nq5Mbza+Auo7N3sQb1QMFaQiDO+4UexWuSGR7Cjb4Sw11SZIJcrrFtiZ+L0jT9MBsUsxDboHVASbCLbE1rnECg==", "requires": { - "@babel/runtime": "^7.14.0" + "@babel/runtime": "^7.16.7" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.17.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.2.tgz", + "integrity": "sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + } } }, "posix-character-classes": { diff --git a/assets/package.json b/assets/package.json index 9b5338e..b52625d 100644 --- a/assets/package.json +++ b/assets/package.json @@ -7,7 +7,7 @@ "watch": "webpack --mode development --watch" }, "dependencies": { - "@intended/intended-ui": "0.1.19", + "@intended/intended-ui": "0.1.21", "phoenix": "file:../deps/phoenix", "phoenix_html": "file:../deps/phoenix_html", "phoenix_live_view": "file:../deps/phoenix_live_view", diff --git a/config/config.exs b/config/config.exs index bee4c18..6cf5b4a 100644 --- a/config/config.exs +++ b/config/config.exs @@ -33,6 +33,9 @@ config :ueberauth, Ueberauth, github: {Ueberauth.Strategy.Github, [default_scope: "user:email", allow_private_emails: true]} ] +config :waffle, + storage: Waffle.Storage.Local + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{Mix.env()}.exs" diff --git a/config/dev.exs b/config/dev.exs index 82dcb83..64dabab 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -18,7 +18,7 @@ config :entendu, Entendu.Repo, config :entendu, EntenduWeb.Endpoint, http: [port: System.get_env("PORT", "4000")], https: [ - port: 4001, + port: 443, cipher_suite: :strong, certfile: "priv/cert/selfsigned.pem", keyfile: "priv/cert/selfsigned_key.pem" diff --git a/lib/entendu/links/link.ex b/lib/entendu/links/link.ex index f36d468..b016de5 100644 --- a/lib/entendu/links/link.ex +++ b/lib/entendu/links/link.ex @@ -1,5 +1,7 @@ defmodule Entendu.Links.Link do use Ecto.Schema + use Waffle.Ecto.Schema + alias Entendu.EncryptedLink import Ecto.Changeset @primary_key {:id, Ecto.UUID, autogenerate: true} @@ -9,8 +11,8 @@ defmodule Entendu.Links.Link do field :expires, :utc_datetime field :filename, :string field :filetype, :string - field :text_content, :string - field :file_content, :string + field :text_content, EncryptedLink.Type + field :file_content, EncryptedLink.Type field :recipient, :string field :service, :string @@ -25,10 +27,10 @@ defmodule Entendu.Links.Link do :burn_after_reading, :filename, :filetype, - :text_content, - :file_content, :recipient, :service ]) + |> cast_attachments(attrs, [:text_content, :file_content]) + end end diff --git a/lib/entendu/user_from_auth.ex b/lib/entendu/user_from_auth.ex index ac67147..a13bada 100644 --- a/lib/entendu/user_from_auth.ex +++ b/lib/entendu/user_from_auth.ex @@ -6,6 +6,7 @@ defmodule Entendu.UserFromAuth do require Jason alias Ueberauth.Auth + alias Entendu.Links.Link def find_or_create(%Auth{} = auth) do {:ok, basic_info(auth)} @@ -24,9 +25,15 @@ defmodule Entendu.UserFromAuth do nil end + defp emails_from_auth(%Auth{ extra: %Auth.Extra{ raw_info: %{ user: %{ "emails" => emails}}}}), do: emails + + defp emails_from_auth(%Auth{ info: %{ email: email }}), do: [email] + + defp emails_from_auth(_auth), do: [] + defp basic_info(auth) do IO.inspect(auth) - %{id: auth.uid, name: name_from_auth(auth), avatar: avatar_from_auth(auth)} + %{id: auth.uid, name: name_from_auth(auth), avatar: avatar_from_auth(auth), emails: emails_from_auth(auth)} end defp name_from_auth(auth) do @@ -44,4 +51,9 @@ defmodule Entendu.UserFromAuth do end end end + + def can_access?(recipient, emails) do + emails + |> Enum.any?(&( &1["verified"] == true and &1["email"] == recipient)) + end end diff --git a/lib/entendu_web/controllers/auth_controller.ex b/lib/entendu_web/controllers/auth_controller.ex index 531e44f..31809a1 100644 --- a/lib/entendu_web/controllers/auth_controller.ex +++ b/lib/entendu_web/controllers/auth_controller.ex @@ -23,18 +23,27 @@ defmodule EntenduWeb.AuthController do end def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do - case UserFromAuth.find_or_create(auth) do - {:ok, user} -> + # TODO: turn this into plug that only proceeds if current_link session var exists + %{ id: link_id, recipient: recipient } = get_session(conn, :current_link) + + with {:ok, user} <- UserFromAuth.find_or_create(auth), + true <- UserFromAuth.can_access?(recipient, user.emails) do + # TODO: send over encrypted data that the frontend can decrypt conn - |> put_flash(:info, "Successfully authenticated.") |> put_session(:current_user, user) |> configure_session(renew: true) - |> redirect(to: "/") + |> redirect(to: "/just/for/you/#{link_id}") + + else + false -> + conn + |> put_flash(:error, "#{recipient} was not found in your list of verified emails") + |> redirect(to: "/just/for/you/#{link_id}") {:error, reason} -> conn |> put_flash(:error, reason) - |> redirect(to: "/") + |> redirect(to: "/just/for/you/#{link_id}") end end end diff --git a/lib/entendu_web/controllers/link_controller.ex b/lib/entendu_web/controllers/link_controller.ex index 981daa7..8692cb3 100644 --- a/lib/entendu_web/controllers/link_controller.ex +++ b/lib/entendu_web/controllers/link_controller.ex @@ -8,7 +8,6 @@ defmodule EntenduWeb.LinkController do alias Entendu.Links alias Links.Link - alias Ecto.Changeset alias EntenduWeb.FallbackController action_fallback(FallbackController) @@ -17,23 +16,13 @@ defmodule EntenduWeb.LinkController do render(conn, "just.html") end - defparams( - first_step(%{ - burn_after_reading: [field: :boolean, default: false], - expires: :utc_datetime, - filename: :string, - filetype: :string, - text_content: :string, - file_content: :string - }) - ) - def just(conn, params) do - with %Changeset{valid?: true} = changeset <- first_step(params), - link_params <- Params.to_map(changeset), - {:ok, %Link{} = link} <- Links.create_link(link_params) do + with {:ok, %Link{} = link} <- Links.create_link(params) do conn |> render("show_authorized.json", %{link: link}) + else + test -> + IO.inspect(test) end end @@ -41,18 +30,9 @@ defmodule EntenduWeb.LinkController do render(conn, "for.html") end - defparams( - second_step(%{ - service: :string, - recipient: :string - }) - ) - - def for(conn, %{link_id: link_id} = params) do - with %Changeset{valid?: true} = changeset <- first_step(params), - link_params <- Params.to_map(changeset), - %Link{} = link <- Links.get_link(link_id), - Links.update_link(link, link_params) do + def for(conn, %{"link_id" => link_id, "recipient" => recipient, "service" => service}) do + with %Link{} = link <- Links.get_link(link_id), + Links.update_link(link, %{ recipient: recipient, service: service}) do conn |> render("show_authorized.json", %{link: link}) end @@ -61,4 +41,12 @@ defmodule EntenduWeb.LinkController do def you_page(conn, _params) do render(conn, "you.html") end + + def auth_page(conn, %{ "id" => link_id}) do + with %Link{service: service, recipient: recipient} = link <- Links.get_link(link_id) do + conn + |> put_session(:current_link, link) + |> render("auth.html", %{ service: service, recipient: recipient }) + end + end end diff --git a/lib/entendu_web/router.ex b/lib/entendu_web/router.ex index 9b93039..ddcb2c0 100644 --- a/lib/entendu_web/router.ex +++ b/lib/entendu_web/router.ex @@ -23,6 +23,7 @@ defmodule EntenduWeb.Router do get "/just/for", LinkController, :for_page post "/just/for", LinkController, :for get "/just/for/you", LinkController, :you_page + get "/just/for/you/:id", LinkController, :auth_page end scope "/auth", EntenduWeb do diff --git a/lib/entendu_web/templates/link/auth.html.eex b/lib/entendu_web/templates/link/auth.html.eex new file mode 100644 index 0000000..c52b8ad --- /dev/null +++ b/lib/entendu_web/templates/link/auth.html.eex @@ -0,0 +1 @@ +<%= react_component("Components.AuthPage", %{ csrf: Plug.CSRFProtection.get_csrf_token(), service: @service, recipient: @recipient, user: @current_user }) %> diff --git a/lib/entendu_web/templates/link/for.html.eex b/lib/entendu_web/templates/link/for.html.eex index de32ab9..e7a513b 100644 --- a/lib/entendu_web/templates/link/for.html.eex +++ b/lib/entendu_web/templates/link/for.html.eex @@ -1 +1 @@ -<%= react_component("Components.ForPage") %> +<%= react_component("Components.ForPage", %{ csrf: Plug.CSRFProtection.get_csrf_token() }) %> diff --git a/lib/entendu_web/uploaders/encrypted_link.ex b/lib/entendu_web/uploaders/encrypted_link.ex new file mode 100644 index 0000000..dcc11ee --- /dev/null +++ b/lib/entendu_web/uploaders/encrypted_link.ex @@ -0,0 +1,56 @@ +defmodule Entendu.EncryptedLink do + use Waffle.Definition + use Waffle.Ecto.Definition + + # Include ecto support (requires package waffle_ecto installed): + # use Waffle.Ecto.Definition + + @versions [:original] + + # To add a thumbnail version: + # @versions [:original, :thumb] + + # Override the bucket on a per definition basis: + # def bucket do + # :custom_bucket_name + # end + + # Whitelist file extensions: + # def validate({file, _}) do + # file_extension = file.file_name |> Path.extname() |> String.downcase() + # + # case Enum.member?(~w(.jpg .jpeg .gif .png), file_extension) do + # true -> :ok + # false -> {:error, "invalid file type"} + # end + # end + + # Define a thumbnail transformation: + # def transform(:thumb, _) do + # {:convert, "-strip -thumbnail 250x250^ -gravity center -extent 250x250 -format png", :png} + # end + + # Override the persisted filenames: + # def filename(version, _) do + # version + # end + + # Override the storage directory: + # def storage_dir(version, {file, scope}) do + # "uploads/user/avatars/#{scope.id}" + # end + + # Provide a default URL if there hasn't been a file uploaded + # def default_url(version, scope) do + # "/images/avatars/default_#{version}.png" + # end + + # Specify custom headers for s3 objects + # Available options are [:cache_control, :content_disposition, + # :content_encoding, :content_length, :content_type, + # :expect, :expires, :storage_class, :website_redirect_location] + # + # def s3_object_headers(version, {file, scope}) do + # [content_type: MIME.from_path(file.file_name)] + # end +end diff --git a/mix.exs b/mix.exs index 72994e7..e3548cb 100644 --- a/mix.exs +++ b/mix.exs @@ -51,7 +51,9 @@ defmodule Entendu.MixProject do {:ueberauth, "~> 0.7.0"}, {:ueberauth_github, "~> 0.8.1"}, {:react_phoenix, "~> 1.3"}, - {:params, "~> 2.2"} + {:params, "~> 2.2"}, + {:waffle, "~> 1.1"}, + {:waffle_ecto, "~> 0.0.11"} ] end diff --git a/mix.lock b/mix.lock index c2af346..cd41c7f 100644 --- a/mix.lock +++ b/mix.lock @@ -42,4 +42,6 @@ "ueberauth": {:hex, :ueberauth, "0.7.0", "9c44f41798b5fa27f872561b6f7d2bb0f10f03fdd22b90f454232d7b087f4b75", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2efad9022e949834f16cc52cd935165049d81fa9e925690f91035c2e4b58d905"}, "ueberauth_github": {:hex, :ueberauth_github, "0.8.1", "0be487b5afc29bc805fa5e31636f37c8f09d5159ef73fc08c4c7a98c9cfe2c18", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7.0", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "143d6130b945ea9bdbd0ef94987f40788f1d7e8090decbfc0722773155e7a74a"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, + "waffle": {:hex, :waffle, "1.1.5", "11b8b41c9dc46a21c8e1e619e1e9048d18d166b57b33d1fada8e11fcd4e678b3", [:mix], [{:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:ex_aws_s3, "~> 2.1", [hex: :ex_aws_s3, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "68e6f92b457b13c71e33cc23f7abb60446a01515dc6618b7d493d8cd466b1f39"}, + "waffle_ecto": {:hex, :waffle_ecto, "0.0.11", "3d9581b3dfc83964ad968ef6bbf31132b5e6959c542a74c49e2a2245a9521048", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:waffle, "~> 1.0", [hex: :waffle, repo: "hexpm", optional: false]}], "hexpm", "626c2832ba94e20840532e609d3af70526d18ff9dfe1b352afb3fbabedb31a7e"}, }