add waffle library, handle file upload, authenticate user with oauth and see if they have the link's email associated to their account

This commit is contained in:
Silas 2022-02-13 00:20:21 -05:00
parent 5de53e23ea
commit cac3757723
Signed by: silentsilas
GPG Key ID: 4199EFB7DAA34349
20 changed files with 305 additions and 94 deletions

View File

@ -14,15 +14,17 @@ import "../css/app.css"
// import {Socket} from "phoenix" // import {Socket} from "phoenix"
// import socket from "./socket" // import socket from "./socket"
// //
import "phoenix_html" import "phoenix_html";
import {Socket} from "phoenix" import {Socket} from "phoenix";
import topbar from "topbar" import topbar from "topbar";
import {LiveSocket} from "phoenix_live_view" import {LiveSocket} from "phoenix_live_view";
import SplashPage from './pages/SplashPage'; import SplashPage from './pages/SplashPage';
import JustPage from './pages/JustPage' import JustPage from './pages/JustPage';
import ForPage from './pages/ForPage' import ForPage from './pages/ForPage';
import YouPage from './pages/YouPage' 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 csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}}) let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})
@ -42,5 +44,5 @@ liveSocket.connect()
window.liveSocket = liveSocket window.liveSocket = liveSocket
window.Components = { window.Components = {
SplashPage, JustPage, ForPage, YouPage SplashPage, JustPage, ForPage, YouPage, AuthPage, RevealPage
} }

View File

@ -1,16 +1,54 @@
// import HexMix from "../utils/hexmix"; import { Button, CenteredContainer, GlobalStyle, Header2, Header3, Input, Label, Spacer, TextAlignWrapper } from "@intended/intended-ui";
// const fragmentData = window.location.hash.split('.'); import React, { useEffect } from "react";
// if (fragmentData.length <= 0) {
// alert("No key found in fragment URI");
// return;
// }
// const key = HexMix.hexToUint8(fragmentData[0]);
// const iv = HexMix.hexToUint8(fragmentData[1]);
// const importedKey = await window.crypto.subtle.importKey( type AuthPageProps = {
// 'raw', csrf: string,
// key, service: string,
// 'AES-GCM', recipient: string
// true, }
// ['encrypt', 'decrypt']
// ); const AuthPage = (props: AuthPageProps) => {
const { service, recipient } = props;
// const [recipientInput, setRecipientInput] = useState("");
// const [serviceSelect, setServiceSelect] = useState("github");
// useEffect(() => {
// }, [])
return (
<React.StrictMode>
<GlobalStyle />
<CenteredContainer fullscreen>
<CenteredContainer>
<Header2 style={{ margin: ".4rem" }}>Someone sent you a secret</Header2>
<Header3 small>
Please verify your identity to reveal this message.
</Header3>
<Spacer space="3rem" />
<TextAlignWrapper align="left">
<Label htmlFor="usernameEmail">Username / Email</Label>
</TextAlignWrapper>
<Input
variant="disabled-medium"
id="usernameEmail"
value={recipient}
/>
<Spacer space="3rem" />
<TextAlignWrapper align="left">
<Label htmlFor="service">Service</Label>
</TextAlignWrapper>
<Input variant="disabled-medium" id="service" value={service} />
<Spacer space="3rem" />
<a href={`https://intended.link/auth/${service}`}>
<Button variant="primary" wide onClick={() => {}}>
Verify
</Button>
</a>
</CenteredContainer>
</CenteredContainer>
</React.StrictMode>
);
}
export default AuthPage;

View File

@ -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"; 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 [recipientInput, setRecipientInput] = useState("");
const [serviceSelect, setServiceSelect] = useState(""); const [serviceSelect, setServiceSelect] = useState("github");
const handleRecipientInputChange = ( const handleRecipientInputChange = (
e: React.ChangeEvent<HTMLInputElement> e: React.ChangeEvent<HTMLInputElement>
@ -18,14 +22,14 @@ const ForPage = () => {
}; };
const postContacts = async () => { const postContacts = async () => {
const fragmentData = window.location.hash.split('.'); // const fragmentData = window.location.hash.split('.');
if (fragmentData.length <= 0) { // if (fragmentData.length <= 0) {
alert("No key found in fragment URI"); // alert("No key found in fragment URI");
return; // return;
} // }
const linkId = sessionStorage.getItem("link_id"); const linkId = sessionStorage.getItem("link_id");
if (linkId == null || linkId == "") { if (!linkId) {
alert("No created link found in storage"); alert("No created link found in storage");
return; return;
} }
@ -36,10 +40,18 @@ const ForPage = () => {
formData.append("link_id", linkId); formData.append("link_id", linkId);
try { 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, body: formData,
method: "POST" 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`; window.location.href = `${window.location.origin}/just/for/you`;
} catch (err: any) { } catch (err: any) {
alert(err.message); alert(err.message);
@ -74,7 +86,9 @@ const ForPage = () => {
id="serviceSelector" id="serviceSelector"
onChange={handleServiceChange} onChange={handleServiceChange}
value={serviceSelect} value={serviceSelect}
/> >
<option value='github'>Github</option>
</Select>
<Spacer space="3rem" /> <Spacer space="3rem" />
<SpaceBetweenContainer> <SpaceBetweenContainer>
<Button variant="secondary" onClick={() => window.location.href = "/just"}> <Button variant="secondary" onClick={() => window.location.href = "/just"}>

View File

@ -1,13 +1,21 @@
import React, { useState } from "react"; import React, { useEffect, useState } from "react";
import { ProgressIndicator, Header2, Button, IconArrow, Label, FileInput, TextArea, CenteredContainer, Spacer, TextAlignWrapper, GlobalStyle } from '@intended/intended-ui'; import { ProgressIndicator, Header2, Button, IconArrow, Label, FileInput, TextArea, CenteredContainer, Spacer, TextAlignWrapper, GlobalStyle } from '@intended/intended-ui';
import HexMix from "../utils/hexmix"; import HexMix from "../utils/hexmix";
const JustPage = (props) => { type JustPageProps = {
csrf: string
}
const JustPage = (props: JustPageProps) => {
const [secretInput, setSecretInput] = useState(""); const [secretInput, setSecretInput] = useState("");
const [fileInput, setFileInput] = useState<File | null>(null); const [fileInput, setFileInput] = useState<File | null>(null);
const [fileName, setFileName] = useState(""); const [fileName, setFileName] = useState("");
useEffect(() => {
sessionStorage.clear();
}, [])
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setSecretInput(e.target.value); setSecretInput(e.target.value);
}; };
@ -51,18 +59,20 @@ const JustPage = (props) => {
const blobData = new Blob([encrypted]); const blobData = new Blob([encrypted]);
formData.append('text_content', blobData); formData.append('text_content', blobData);
formData.append('filetype', 'text/plain'); // formData.append('filetype', 'text/plain');
formData.append('filename', 'secret.txt'); // formData.append('filename', 'secret.txt');
try { try {
const link: unknown = await fetch(`${window.location.origin}/just`, { const link: Response = await fetch(`${window.location.origin}/just`, {
headers: { headers: {
"X-CSRF-Token": props.csrf "X-CSRF-Token": props.csrf
}, },
body: formData, body: formData,
method: "POST" method: "POST"
}); });
sessionStorage.setItem("link_id", (link as Link).id); const { id: link_id } = await link.json()
sessionStorage.setItem("link_id", link_id);
sessionStorage.setItem("key_hex", keyHex); sessionStorage.setItem("key_hex", keyHex);
sessionStorage.setItem("iv_hex", ivHex); sessionStorage.setItem("iv_hex", ivHex);
window.location.href = `${window.location.origin}/just/for`; window.location.href = `${window.location.origin}/just/for`;

View File

@ -0,0 +1,61 @@
import { Button, CenteredContainer, GlobalStyle, Header2, Header3, InputButtonWithIcon, Label, SpaceBetweenContainer, Spacer, TextAlignWrapper, TextAreaParagraph } from "@intended/intended-ui";
import React, { useEffect, useState } from "react";
// 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]);
// const importedKey = await window.crypto.subtle.importKey(
// 'raw',
// key,
// 'AES-GCM',
// true,
// ['encrypt', 'decrypt']
// );
const RevealPage = () => {
return (
<React.StrictMode>
<GlobalStyle />
<CenteredContainer fullscreen>
<CenteredContainer>
<Header2 style={{ margin: ".4rem" }}>Someone sent you a secret</Header2>
<Header3 small>
Please verify your identity to reveal this message.
</Header3>
<Spacer space="3rem" />
<SpaceBetweenContainer>
<Label htmlFor="secretMessage">Secret message</Label>
<Label htmlFor="secretMessage">Sent 8/24/21 @ 1:27pm</Label>
</SpaceBetweenContainer>
<TextAreaParagraph id="secretMessage">
"Sup. What are you doing for lunch?"
</TextAreaParagraph>
<Spacer space="3rem" />
<TextAlignWrapper align="left">
<Label htmlFor="service">Secret File</Label>
</TextAlignWrapper>
<InputButtonWithIcon
variant="download"
id="downloadfile"
value="1780983.jpg"
onClick={() => {}}
/>
<Spacer space="3rem" />
<Button variant="secondary" wide onClick={() => {}}>
Send a secret
</Button>
</CenteredContainer>
</CenteredContainer>
</React.StrictMode>
);
}
export default RevealPage;

View File

@ -8,11 +8,10 @@ const YouPage = () => {
const keyHex = sessionStorage.getItem("key_hex"); const keyHex = sessionStorage.getItem("key_hex");
const ivHex = sessionStorage.getItem("iv_hex"); const ivHex = sessionStorage.getItem("iv_hex");
`${window.location.origin}/just/for/you/${linkId}#${keyHex}.${ivHex}` return `${window.location.origin}/just/for/you/${linkId}#${keyHex}.${ivHex}`;
return "";
}; };
const [url, setUrl] = useState(calculateUrl()); const [url, _setUrl] = useState(calculateUrl());
const copyUrl = async () => { const copyUrl = async () => {
try { try {
@ -51,7 +50,7 @@ const YouPage = () => {
<Input <Input
variant="disabled-light" variant="disabled-light"
id="encodedSecret" id="encodedSecret"
value="\4š€Š”Çm’ÄyÆFՑ¬Ð$CÑӀÃyۄ" value={url}
/> />
<Spacer space="3rem" /> <Spacer space="3rem" />
<SpaceBetweenContainer> <SpaceBetweenContainer>

View File

@ -1079,6 +1079,7 @@
"version": "7.16.3", "version": "7.16.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz",
"integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==", "integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==",
"dev": true,
"requires": { "requires": {
"regenerator-runtime": "^0.13.4" "regenerator-runtime": "^0.13.4"
} }
@ -1142,9 +1143,9 @@
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
}, },
"@intended/intended-ui": { "@intended/intended-ui": {
"version": "0.1.19", "version": "0.1.21",
"resolved": "https://registry.npmjs.org/@intended/intended-ui/-/intended-ui-0.1.19.tgz", "resolved": "https://registry.npmjs.org/@intended/intended-ui/-/intended-ui-0.1.21.tgz",
"integrity": "sha512-dOQZBvBm5UyKhyilTE6y/3KL1suyjbAuu+kn4b3tKhpy/Y23AglVlHLj4oi/AUgGRMtcRT/o9eySJWkNWQ9weA==", "integrity": "sha512-rNyJOGLOw8iKP0AaSYSytZXFssPhlE1FbUjxEze8F4iOgXxs6iu7B9+gSIF4QJg/IrQUVu76tkevBGDk2nTQNg==",
"requires": { "requires": {
"polished": "^4.1.3", "polished": "^4.1.3",
"react": "^17.0.2", "react": "^17.0.2",
@ -1719,9 +1720,9 @@
} }
}, },
"babel-plugin-styled-components": { "babel-plugin-styled-components": {
"version": "2.0.1", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.1.tgz", "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.2.tgz",
"integrity": "sha512-U3wmORxerYBiqcRCo6thItIosEIga3F+ph0jJPkiOZJjyhpZyUZFQV9XvrZ2CbBIihJ3rDBC/itQ+Wx3VHMauw==", "integrity": "sha512-7eG5NE8rChnNTDxa6LQfynwgHTVOYYaHJbUYSlOhk8QBXIQiMBKq4gyfHBBKPrxUcVBXVJL61ihduCpCQbuNbw==",
"requires": { "requires": {
"@babel/helper-annotate-as-pure": "^7.16.0", "@babel/helper-annotate-as-pure": "^7.16.0",
"@babel/helper-module-imports": "^7.16.0", "@babel/helper-module-imports": "^7.16.0",
@ -5896,11 +5897,21 @@
} }
}, },
"polished": { "polished": {
"version": "4.1.3", "version": "4.1.4",
"resolved": "https://registry.npmjs.org/polished/-/polished-4.1.3.tgz", "resolved": "https://registry.npmjs.org/polished/-/polished-4.1.4.tgz",
"integrity": "sha512-ocPAcVBUOryJEKe0z2KLd1l9EBa1r5mSwlKpExmrLzsnIzJo4axsoU9O2BjOTkDGDT4mZ0WFE5XKTlR3nLnZOA==", "integrity": "sha512-Nq5Mbza+Auo7N3sQb1QMFaQiDO+4UexWuSGR7Cjb4Sw11SZIJcrrFtiZ+L0jT9MBsUsxDboHVASbCLbE1rnECg==",
"requires": { "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": { "posix-character-classes": {

View File

@ -7,7 +7,7 @@
"watch": "webpack --mode development --watch" "watch": "webpack --mode development --watch"
}, },
"dependencies": { "dependencies": {
"@intended/intended-ui": "0.1.19", "@intended/intended-ui": "0.1.21",
"phoenix": "file:../deps/phoenix", "phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html", "phoenix_html": "file:../deps/phoenix_html",
"phoenix_live_view": "file:../deps/phoenix_live_view", "phoenix_live_view": "file:../deps/phoenix_live_view",

View File

@ -33,6 +33,9 @@ config :ueberauth, Ueberauth,
github: {Ueberauth.Strategy.Github, [default_scope: "user:email", allow_private_emails: true]} 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 # Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above. # of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs" import_config "#{Mix.env()}.exs"

View File

@ -18,7 +18,7 @@ config :entendu, Entendu.Repo,
config :entendu, EntenduWeb.Endpoint, config :entendu, EntenduWeb.Endpoint,
http: [port: System.get_env("PORT", "4000")], http: [port: System.get_env("PORT", "4000")],
https: [ https: [
port: 4001, port: 443,
cipher_suite: :strong, cipher_suite: :strong,
certfile: "priv/cert/selfsigned.pem", certfile: "priv/cert/selfsigned.pem",
keyfile: "priv/cert/selfsigned_key.pem" keyfile: "priv/cert/selfsigned_key.pem"

View File

@ -1,5 +1,7 @@
defmodule Entendu.Links.Link do defmodule Entendu.Links.Link do
use Ecto.Schema use Ecto.Schema
use Waffle.Ecto.Schema
alias Entendu.EncryptedLink
import Ecto.Changeset import Ecto.Changeset
@primary_key {:id, Ecto.UUID, autogenerate: true} @primary_key {:id, Ecto.UUID, autogenerate: true}
@ -9,8 +11,8 @@ defmodule Entendu.Links.Link do
field :expires, :utc_datetime field :expires, :utc_datetime
field :filename, :string field :filename, :string
field :filetype, :string field :filetype, :string
field :text_content, :string field :text_content, EncryptedLink.Type
field :file_content, :string field :file_content, EncryptedLink.Type
field :recipient, :string field :recipient, :string
field :service, :string field :service, :string
@ -25,10 +27,10 @@ defmodule Entendu.Links.Link do
:burn_after_reading, :burn_after_reading,
:filename, :filename,
:filetype, :filetype,
:text_content,
:file_content,
:recipient, :recipient,
:service :service
]) ])
|> cast_attachments(attrs, [:text_content, :file_content])
end end
end end

View File

@ -6,6 +6,7 @@ defmodule Entendu.UserFromAuth do
require Jason require Jason
alias Ueberauth.Auth alias Ueberauth.Auth
alias Entendu.Links.Link
def find_or_create(%Auth{} = auth) do def find_or_create(%Auth{} = auth) do
{:ok, basic_info(auth)} {:ok, basic_info(auth)}
@ -24,9 +25,15 @@ defmodule Entendu.UserFromAuth do
nil nil
end 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 defp basic_info(auth) do
IO.inspect(auth) 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 end
defp name_from_auth(auth) do defp name_from_auth(auth) do
@ -44,4 +51,9 @@ defmodule Entendu.UserFromAuth do
end end
end end
end end
def can_access?(recipient, emails) do
emails
|> Enum.any?(&( &1["verified"] == true and &1["email"] == recipient))
end
end end

View File

@ -23,18 +23,27 @@ defmodule EntenduWeb.AuthController do
end end
def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
case UserFromAuth.find_or_create(auth) do # TODO: turn this into plug that only proceeds if current_link session var exists
{:ok, user} -> %{ 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 conn
|> put_flash(:info, "Successfully authenticated.")
|> put_session(:current_user, user) |> put_session(:current_user, user)
|> configure_session(renew: true) |> 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} -> {:error, reason} ->
conn conn
|> put_flash(:error, reason) |> put_flash(:error, reason)
|> redirect(to: "/") |> redirect(to: "/just/for/you/#{link_id}")
end end
end end
end end

View File

@ -8,7 +8,6 @@ defmodule EntenduWeb.LinkController do
alias Entendu.Links alias Entendu.Links
alias Links.Link alias Links.Link
alias Ecto.Changeset
alias EntenduWeb.FallbackController alias EntenduWeb.FallbackController
action_fallback(FallbackController) action_fallback(FallbackController)
@ -17,23 +16,13 @@ defmodule EntenduWeb.LinkController do
render(conn, "just.html") render(conn, "just.html")
end 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 def just(conn, params) do
with %Changeset{valid?: true} = changeset <- first_step(params), with {:ok, %Link{} = link} <- Links.create_link(params) do
link_params <- Params.to_map(changeset),
{:ok, %Link{} = link} <- Links.create_link(link_params) do
conn conn
|> render("show_authorized.json", %{link: link}) |> render("show_authorized.json", %{link: link})
else
test ->
IO.inspect(test)
end end
end end
@ -41,18 +30,9 @@ defmodule EntenduWeb.LinkController do
render(conn, "for.html") render(conn, "for.html")
end end
defparams( def for(conn, %{"link_id" => link_id, "recipient" => recipient, "service" => service}) do
second_step(%{ with %Link{} = link <- Links.get_link(link_id),
service: :string, Links.update_link(link, %{ recipient: recipient, service: service}) do
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
conn conn
|> render("show_authorized.json", %{link: link}) |> render("show_authorized.json", %{link: link})
end end
@ -61,4 +41,12 @@ defmodule EntenduWeb.LinkController do
def you_page(conn, _params) do def you_page(conn, _params) do
render(conn, "you.html") render(conn, "you.html")
end 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 end

View File

@ -23,6 +23,7 @@ defmodule EntenduWeb.Router do
get "/just/for", LinkController, :for_page get "/just/for", LinkController, :for_page
post "/just/for", LinkController, :for post "/just/for", LinkController, :for
get "/just/for/you", LinkController, :you_page get "/just/for/you", LinkController, :you_page
get "/just/for/you/:id", LinkController, :auth_page
end end
scope "/auth", EntenduWeb do scope "/auth", EntenduWeb do

View File

@ -0,0 +1 @@
<%= react_component("Components.AuthPage", %{ csrf: Plug.CSRFProtection.get_csrf_token(), service: @service, recipient: @recipient, user: @current_user }) %>

View File

@ -1 +1 @@
<%= react_component("Components.ForPage") %> <%= react_component("Components.ForPage", %{ csrf: Plug.CSRFProtection.get_csrf_token() }) %>

View File

@ -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

View File

@ -51,7 +51,9 @@ defmodule Entendu.MixProject do
{:ueberauth, "~> 0.7.0"}, {:ueberauth, "~> 0.7.0"},
{:ueberauth_github, "~> 0.8.1"}, {:ueberauth_github, "~> 0.8.1"},
{:react_phoenix, "~> 1.3"}, {:react_phoenix, "~> 1.3"},
{:params, "~> 2.2"} {:params, "~> 2.2"},
{:waffle, "~> 1.1"},
{:waffle_ecto, "~> 0.0.11"}
] ]
end end

View File

@ -42,4 +42,6 @@
"ueberauth": {:hex, :ueberauth, "0.7.0", "9c44f41798b5fa27f872561b6f7d2bb0f10f03fdd22b90f454232d7b087f4b75", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2efad9022e949834f16cc52cd935165049d81fa9e925690f91035c2e4b58d905"}, "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"}, "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"}, "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"},
} }