fix file uploads, serve them properly, put behind auth wall, decrypt secret message in frontend
This commit is contained in:
parent
cac3757723
commit
2ac596b8c8
|
@ -35,3 +35,5 @@ npm-debug.log
|
||||||
/priv/cert/
|
/priv/cert/
|
||||||
|
|
||||||
dev.secret.exs
|
dev.secret.exs
|
||||||
|
/uploads/*
|
||||||
|
!/uploads/.gitkeep
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
}
|
|
@ -24,7 +24,6 @@ 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 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}})
|
||||||
|
@ -44,5 +43,5 @@ liveSocket.connect()
|
||||||
window.liveSocket = liveSocket
|
window.liveSocket = liveSocket
|
||||||
|
|
||||||
window.Components = {
|
window.Components = {
|
||||||
SplashPage, JustPage, ForPage, YouPage, AuthPage, RevealPage
|
SplashPage, JustPage, ForPage, YouPage, AuthPage
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
interface Link {
|
type IntendedUser = {
|
||||||
id: string
|
name: string;
|
||||||
}
|
emails: string[];
|
||||||
|
};
|
||||||
|
|
|
@ -1,54 +1,219 @@
|
||||||
import { Button, CenteredContainer, GlobalStyle, Header2, Header3, Input, Label, Spacer, TextAlignWrapper } from "@intended/intended-ui";
|
import {
|
||||||
import React, { useEffect } from "react";
|
Button,
|
||||||
|
CenteredContainer,
|
||||||
|
GlobalStyle,
|
||||||
|
Header2,
|
||||||
|
Header3,
|
||||||
|
Input,
|
||||||
|
InputButtonWithIcon,
|
||||||
|
Label,
|
||||||
|
SpaceBetweenContainer,
|
||||||
|
Spacer,
|
||||||
|
TextAlignWrapper,
|
||||||
|
TextAreaParagraph,
|
||||||
|
} from "@intended/intended-ui";
|
||||||
|
import HexMix from "../utils/hexmix";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
type AuthPageProps = {
|
type AuthPageProps = {
|
||||||
csrf: string,
|
csrf: string;
|
||||||
service: string,
|
service: string;
|
||||||
recipient: string
|
recipient: string;
|
||||||
|
user: IntendedUser | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Keys {
|
||||||
|
key: string;
|
||||||
|
iv: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Link {
|
||||||
|
text: Blob | null;
|
||||||
|
file: Blob | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthPage = (props: AuthPageProps) => {
|
const AuthPage = (props: AuthPageProps) => {
|
||||||
const { service, recipient } = props;
|
const { service, recipient, user } = props;
|
||||||
// const [recipientInput, setRecipientInput] = useState("");
|
|
||||||
// const [serviceSelect, setServiceSelect] = useState("github");
|
|
||||||
|
|
||||||
// useEffect(() => {
|
const [secretFileUrl, _setsecretFileUrl] = useState<string>("#");
|
||||||
|
const [secretMessage, setSecretMessage] = useState<string>("Decrypting...");
|
||||||
|
|
||||||
// }, [])
|
useEffect(() => {
|
||||||
|
init().catch((reason) => {
|
||||||
|
alert(reason);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
const init = async (): Promise<void> => {
|
||||||
<React.StrictMode>
|
const keys: Keys | null = await retrieveKeys();
|
||||||
<GlobalStyle />
|
const link: Link | null = await retrieveLink();
|
||||||
<CenteredContainer fullscreen>
|
if (link && keys) {
|
||||||
<CenteredContainer>
|
await decrypt(link, keys);
|
||||||
<Header2 style={{ margin: ".4rem" }}>Someone sent you a secret</Header2>
|
}
|
||||||
<Header3 small>
|
};
|
||||||
Please verify your identity to reveal this message.
|
|
||||||
</Header3>
|
const retrieveLink = async (): Promise<Link | null> => {
|
||||||
<Spacer space="3rem" />
|
const urlSegments = new URL(document.URL).pathname.split("/");
|
||||||
<TextAlignWrapper align="left">
|
const linkId = urlSegments.pop() || urlSegments.pop();
|
||||||
<Label htmlFor="usernameEmail">Username / Email</Label>
|
if (!linkId) {
|
||||||
</TextAlignWrapper>
|
alert("Could not find intended link in URL");
|
||||||
<Input
|
return null;
|
||||||
variant="disabled-medium"
|
}
|
||||||
id="usernameEmail"
|
|
||||||
value={recipient}
|
const textResponse = await fetch(`/uploads/links/${linkId}/text`);
|
||||||
/>
|
const textData = await textResponse.blob();
|
||||||
<Spacer space="3rem" />
|
const fileResponse = await fetch(`/uploads/links/${linkId}/file`);
|
||||||
<TextAlignWrapper align="left">
|
const fileData = await fileResponse.blob();
|
||||||
<Label htmlFor="service">Service</Label>
|
|
||||||
</TextAlignWrapper>
|
return {
|
||||||
<Input variant="disabled-medium" id="service" value={service} />
|
text: textData.size > 0 ? textData : null,
|
||||||
<Spacer space="3rem" />
|
file: fileData.size > 0 ? fileData : null,
|
||||||
<a href={`https://intended.link/auth/${service}`}>
|
};
|
||||||
<Button variant="primary" wide onClick={() => {}}>
|
};
|
||||||
Verify
|
|
||||||
</Button>
|
const retrieveKeys = async (): Promise<Keys | null> => {
|
||||||
</a>
|
const fragmentData = window.location.hash.split(".");
|
||||||
</CenteredContainer>
|
let key, iv;
|
||||||
</CenteredContainer>
|
|
||||||
</React.StrictMode>
|
// remove the hash from fragment URI
|
||||||
|
fragmentData[0] = fragmentData[0].slice(1);
|
||||||
|
|
||||||
|
if (fragmentData.length <= 1) {
|
||||||
|
key = sessionStorage.getItem("link_key");
|
||||||
|
iv = sessionStorage.getItem("link_iv");
|
||||||
|
} else {
|
||||||
|
key = fragmentData[0];
|
||||||
|
iv = fragmentData[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key && iv) {
|
||||||
|
return { key: key, iv: iv };
|
||||||
|
} else {
|
||||||
|
alert("No key found in fragment URI or session storage.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const decrypt = async (link: Link, keys: Keys) => {
|
||||||
|
const convertedKey = HexMix.hexToUint8(keys.key);
|
||||||
|
const convertedIv = HexMix.hexToUint8(keys.iv);
|
||||||
|
const importedKey = await window.crypto.subtle.importKey(
|
||||||
|
"raw",
|
||||||
|
convertedKey,
|
||||||
|
"AES-GCM",
|
||||||
|
true,
|
||||||
|
["encrypt", "decrypt"]
|
||||||
);
|
);
|
||||||
}
|
if (link?.text) {
|
||||||
|
const textFile = await link.text.arrayBuffer();
|
||||||
|
const encodedText = await window.crypto.subtle.decrypt(
|
||||||
|
{
|
||||||
|
name: "AES-GCM",
|
||||||
|
length: 256,
|
||||||
|
iv: convertedIv,
|
||||||
|
},
|
||||||
|
importedKey,
|
||||||
|
textFile
|
||||||
|
);
|
||||||
|
// And voila
|
||||||
|
HexMix.arrayBufferToString(encodedText, (result: string) => {
|
||||||
|
setSecretMessage(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (link?.file) {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderHeader = (): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Header2 style={{ margin: ".4rem" }}>Someone sent you a secret</Header2>
|
||||||
|
{user ? (
|
||||||
|
<Header3 small>
|
||||||
|
Hello {user.name}, you are logged in {service} which has the
|
||||||
|
following verified emails: {user.emails.join(", ")}
|
||||||
|
</Header3>
|
||||||
|
) : (
|
||||||
|
<Header3 small>
|
||||||
|
Please verify your identity to reveal this message.
|
||||||
|
</Header3>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderAuth = (): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<CenteredContainer fullscreen>
|
||||||
|
<CenteredContainer>
|
||||||
|
{renderHeader()}
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderReveal = (): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<CenteredContainer fullscreen>
|
||||||
|
<CenteredContainer>
|
||||||
|
{renderHeader()}
|
||||||
|
<Spacer space="3rem" />
|
||||||
|
|
||||||
|
<SpaceBetweenContainer>
|
||||||
|
<Label htmlFor="secretMessage">Secret message</Label>
|
||||||
|
<Label htmlFor="secretMessage">Sent 8/24/21 @ 1:27pm</Label>
|
||||||
|
</SpaceBetweenContainer>
|
||||||
|
<TextAreaParagraph id="secretMessage">
|
||||||
|
{secretMessage}
|
||||||
|
</TextAreaParagraph>
|
||||||
|
|
||||||
|
<Spacer space="3rem" />
|
||||||
|
<TextAlignWrapper align="left">
|
||||||
|
<Label htmlFor="service">Secret File</Label>
|
||||||
|
</TextAlignWrapper>
|
||||||
|
<InputButtonWithIcon
|
||||||
|
variant="download"
|
||||||
|
id="downloadfile"
|
||||||
|
value={secretFileUrl}
|
||||||
|
onClick={() => {}}
|
||||||
|
/>
|
||||||
|
<Spacer space="3rem" />
|
||||||
|
<a href={`https://intended.link/auth/${service}`}>
|
||||||
|
<Button variant="primary" wide onClick={() => {}}>
|
||||||
|
Re-Verify
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</CenteredContainer>
|
||||||
|
</CenteredContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.StrictMode>
|
||||||
|
<GlobalStyle />
|
||||||
|
{user ? renderReveal() : renderAuth()}
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default AuthPage;
|
export default AuthPage;
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
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;
|
|
|
@ -14,7 +14,7 @@ config :entendu, Entendu.Repo, migration_primary_key: [type: :uuid]
|
||||||
|
|
||||||
# Configures the endpoint
|
# Configures the endpoint
|
||||||
config :entendu, EntenduWeb.Endpoint,
|
config :entendu, EntenduWeb.Endpoint,
|
||||||
url: [host: "dev.intended.link"],
|
url: [host: "intended.link"],
|
||||||
secret_key_base: "6PqoqDqHzsXs6pcm/QoI48rR0paD0gxubXBaR6j/b1fJNgL6Fawn5JPl82N/M2NR",
|
secret_key_base: "6PqoqDqHzsXs6pcm/QoI48rR0paD0gxubXBaR6j/b1fJNgL6Fawn5JPl82N/M2NR",
|
||||||
render_errors: [view: EntenduWeb.ErrorView, accepts: ~w(html json), layout: false],
|
render_errors: [view: EntenduWeb.ErrorView, accepts: ~w(html json), layout: false],
|
||||||
pubsub_server: Entendu.PubSub,
|
pubsub_server: Entendu.PubSub,
|
||||||
|
|
|
@ -51,9 +51,10 @@ defmodule Entendu.Links do
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def create_link(attrs \\ %{}) do
|
def create_link(attrs \\ %{}) do
|
||||||
%Link{}
|
Ecto.Multi.new()
|
||||||
|> Link.changeset(attrs)
|
|> Ecto.Multi.insert(:link, Link.changeset(%Link{}, attrs))
|
||||||
|> Repo.insert()
|
|> Ecto.Multi.update(:link_with_file, &Link.file_changeset(&1.link, attrs))
|
||||||
|
|> Repo.transaction()
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
|
@ -5,7 +5,17 @@ defmodule Entendu.Links.Link do
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
|
||||||
@primary_key {:id, Ecto.UUID, autogenerate: true}
|
@primary_key {:id, Ecto.UUID, autogenerate: true}
|
||||||
|
@derive {Jason.Encoder,
|
||||||
|
only: [
|
||||||
|
:burn_after_reading,
|
||||||
|
:expires,
|
||||||
|
:filename,
|
||||||
|
:filetype,
|
||||||
|
:text_content,
|
||||||
|
:file_content,
|
||||||
|
:recipient,
|
||||||
|
:service
|
||||||
|
]}
|
||||||
schema "links" do
|
schema "links" do
|
||||||
field :burn_after_reading, :boolean, default: false
|
field :burn_after_reading, :boolean, default: false
|
||||||
field :expires, :utc_datetime
|
field :expires, :utc_datetime
|
||||||
|
@ -30,7 +40,10 @@ defmodule Entendu.Links.Link do
|
||||||
:recipient,
|
:recipient,
|
||||||
:service
|
:service
|
||||||
])
|
])
|
||||||
|> cast_attachments(attrs, [:text_content, :file_content])
|
end
|
||||||
|
|
||||||
|
def file_changeset(link, attrs) do
|
||||||
|
link
|
||||||
|
|> cast_attachments(attrs, [:text_content, :file_content])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -25,15 +25,21 @@ defmodule Entendu.UserFromAuth do
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
defp emails_from_auth(%Auth{ extra: %Auth.Extra{ raw_info: %{ user: %{ "emails" => emails}}}}), do: emails
|
# github
|
||||||
|
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{info: %{email: email}}), do: [email]
|
||||||
|
|
||||||
defp emails_from_auth(_auth), do: []
|
defp emails_from_auth(_auth), do: []
|
||||||
|
|
||||||
defp basic_info(auth) do
|
defp basic_info(auth) do
|
||||||
IO.inspect(auth)
|
%{
|
||||||
%{id: auth.uid, name: name_from_auth(auth), avatar: avatar_from_auth(auth), emails: emails_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
|
||||||
|
@ -54,6 +60,6 @@ defmodule Entendu.UserFromAuth do
|
||||||
|
|
||||||
def can_access?(recipient, emails) do
|
def can_access?(recipient, emails) do
|
||||||
emails
|
emails
|
||||||
|> Enum.any?(&( &1["verified"] == true and &1["email"] == recipient))
|
|> Enum.any?(&(&1["verified"] == true and &1["email"] == recipient))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -39,6 +39,14 @@ defmodule EntenduWeb do
|
||||||
|
|
||||||
import ReactPhoenix.ClientSide
|
import ReactPhoenix.ClientSide
|
||||||
|
|
||||||
|
def current_user(conn) do
|
||||||
|
Plug.Conn.get_session(conn, :current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_link(conn) do
|
||||||
|
Plug.Conn.get_session(conn, :current_link)
|
||||||
|
end
|
||||||
|
|
||||||
# Include shared imports and aliases for views
|
# Include shared imports and aliases for views
|
||||||
unquote(view_helpers())
|
unquote(view_helpers())
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,6 +8,8 @@ defmodule EntenduWeb.AuthController do
|
||||||
plug Ueberauth
|
plug Ueberauth
|
||||||
|
|
||||||
alias Entendu.UserFromAuth
|
alias Entendu.UserFromAuth
|
||||||
|
alias EntenduWeb.LinkView
|
||||||
|
alias Entendu.EncryptedLink
|
||||||
|
|
||||||
def delete(conn, _params) do
|
def delete(conn, _params) do
|
||||||
conn
|
conn
|
||||||
|
@ -23,27 +25,32 @@ defmodule EntenduWeb.AuthController do
|
||||||
end
|
end
|
||||||
|
|
||||||
def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
|
def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
|
||||||
# TODO: turn this into plug that only proceeds if current_link session var exists
|
link = get_session(conn, :intended_link)
|
||||||
%{ id: link_id, recipient: recipient } = get_session(conn, :current_link)
|
|
||||||
|
|
||||||
with {:ok, user} <- UserFromAuth.find_or_create(auth),
|
with %{id: link_id, recipient: recipient} <- link,
|
||||||
true <- UserFromAuth.can_access?(recipient, user.emails) do
|
{:ok, user} <- UserFromAuth.find_or_create(auth),
|
||||||
# TODO: send over encrypted data that the frontend can decrypt
|
true <- UserFromAuth.can_access?(recipient, user.emails) do
|
||||||
conn
|
# TODO: send over encrypted data that the frontend can decrypt
|
||||||
|> put_session(:current_user, user)
|
|
||||||
|> configure_session(renew: true)
|
|
||||||
|> redirect(to: "/just/for/you/#{link_id}")
|
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_session(:current_user, user)
|
||||||
|
|> configure_session(renew: true)
|
||||||
|
|> redirect(to: "/just/for/you/#{link_id}")
|
||||||
else
|
else
|
||||||
|
nil ->
|
||||||
|
conn
|
||||||
|
|> put_flash(:error, "Could not find link to authenticate against")
|
||||||
|
|> redirect(to: "/just/for/you/")
|
||||||
|
|
||||||
false ->
|
false ->
|
||||||
conn
|
conn
|
||||||
|> put_flash(:error, "#{recipient} was not found in your list of verified emails")
|
|> put_flash(:error, "#{link.recipient} was not found in your list of verified emails")
|
||||||
|> redirect(to: "/just/for/you/#{link_id}")
|
|> redirect(to: "/just/for/you/#{link.id}")
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
conn
|
conn
|
||||||
|> put_flash(:error, reason)
|
|> put_flash(:error, reason)
|
||||||
|> redirect(to: "/just/for/you/#{link_id}")
|
|> redirect(to: "/just/for/you/#{link.id}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
defmodule EntenduWeb.FileNotFoundController do
|
||||||
|
use EntenduWeb, :controller
|
||||||
|
|
||||||
|
def index(conn, _params) do
|
||||||
|
conn
|
||||||
|
|> put_status(404)
|
||||||
|
|> text("File Not Found")
|
||||||
|
end
|
||||||
|
end
|
|
@ -9,6 +9,11 @@ defmodule EntenduWeb.LinkController do
|
||||||
alias Entendu.Links
|
alias Entendu.Links
|
||||||
alias Links.Link
|
alias Links.Link
|
||||||
alias EntenduWeb.FallbackController
|
alias EntenduWeb.FallbackController
|
||||||
|
alias Entendu.EncryptedLink
|
||||||
|
alias Entendu.UserFromAuth
|
||||||
|
alias EntenduWeb.Plugs.AuthorizeLink
|
||||||
|
|
||||||
|
plug AuthorizeLink when action in [:text, :file]
|
||||||
|
|
||||||
action_fallback(FallbackController)
|
action_fallback(FallbackController)
|
||||||
|
|
||||||
|
@ -17,12 +22,9 @@ defmodule EntenduWeb.LinkController do
|
||||||
end
|
end
|
||||||
|
|
||||||
def just(conn, params) do
|
def just(conn, params) do
|
||||||
with {:ok, %Link{} = link} <- Links.create_link(params) do
|
with {:ok, %{link_with_file: %Link{} = link}} <- Links.create_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
|
||||||
|
|
||||||
|
@ -32,7 +34,7 @@ defmodule EntenduWeb.LinkController do
|
||||||
|
|
||||||
def for(conn, %{"link_id" => link_id, "recipient" => recipient, "service" => service}) do
|
def for(conn, %{"link_id" => link_id, "recipient" => recipient, "service" => service}) do
|
||||||
with %Link{} = link <- Links.get_link(link_id),
|
with %Link{} = link <- Links.get_link(link_id),
|
||||||
Links.update_link(link, %{ recipient: recipient, service: service}) do
|
Links.update_link(link, %{recipient: recipient, service: service}) do
|
||||||
conn
|
conn
|
||||||
|> render("show_authorized.json", %{link: link})
|
|> render("show_authorized.json", %{link: link})
|
||||||
end
|
end
|
||||||
|
@ -42,11 +44,29 @@ defmodule EntenduWeb.LinkController do
|
||||||
render(conn, "you.html")
|
render(conn, "you.html")
|
||||||
end
|
end
|
||||||
|
|
||||||
def auth_page(conn, %{ "id" => link_id}) do
|
def auth_page(conn, %{"id" => link_id}) do
|
||||||
with %Link{service: service, recipient: recipient} = link <- Links.get_link(link_id) do
|
with %Link{service: service, recipient: recipient} = link <- Links.get_link(link_id) do
|
||||||
conn
|
conn
|
||||||
|> put_session(:current_link, link)
|
|> put_session(:intended_link, link)
|
||||||
|> render("auth.html", %{ service: service, recipient: recipient })
|
|> render("auth.html", %{intended_link: %{service: service, recipient: recipient}})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def text(conn, %{"id" => link_id}) do
|
||||||
|
with user = get_session(conn, :current_user),
|
||||||
|
%Link{recipient: recipient} = link <- Links.get_link(link_id),
|
||||||
|
true <- UserFromAuth.can_access?(recipient, user.emails) do
|
||||||
|
path = EncryptedLink.url({link.text_content, link})
|
||||||
|
send_file(conn, 200, path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def file(conn, %{"id" => link_id}) do
|
||||||
|
with user = get_session(conn, :current_user),
|
||||||
|
%Link{recipient: recipient} = link <- Links.get_link(link_id),
|
||||||
|
true <- UserFromAuth.can_access?(recipient, user.emails) do
|
||||||
|
path = EncryptedLink.url({link.file_content, link})
|
||||||
|
send_file(conn, 200, path)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
defmodule EntenduWeb.Plugs.AuthorizeLink do
|
||||||
|
import Plug.Conn
|
||||||
|
use EntenduWeb, :controller
|
||||||
|
|
||||||
|
alias Entendu.Repo
|
||||||
|
alias Entendu.UserFromAuth
|
||||||
|
alias Entendu.Links
|
||||||
|
alias Entendu.Links.Link
|
||||||
|
alias EntenduWeb.FallbackController
|
||||||
|
alias EntenduWeb.ErrorView
|
||||||
|
|
||||||
|
def init(_params) do
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(conn, params) do
|
||||||
|
%{params: %{"path" => [_, link_id, _]}} = conn
|
||||||
|
user = get_session(conn, :current_user)
|
||||||
|
|
||||||
|
if !user do
|
||||||
|
conn
|
||||||
|
|> put_status(403)
|
||||||
|
|> put_view(EntenduWeb.ErrorView)
|
||||||
|
|> render("error_code.json", message: "Unauthorized", code: 403)
|
||||||
|
|> halt
|
||||||
|
else
|
||||||
|
with {:ok, user} <- get_user_from_path(conn),
|
||||||
|
%Link{recipient: recipient} = link <- Links.get_link(link_id),
|
||||||
|
true <- UserFromAuth.can_access?(recipient, user.emails) do
|
||||||
|
conn
|
||||||
|
|> assign(:link, link)
|
||||||
|
else
|
||||||
|
nil ->
|
||||||
|
conn
|
||||||
|
|> put_status(404)
|
||||||
|
|> put_view(EntenduWeb.ErrorView)
|
||||||
|
|> render("error_code.json", message: "Link could not be found", code: 404)
|
||||||
|
|> halt
|
||||||
|
|
||||||
|
false ->
|
||||||
|
conn
|
||||||
|
|> put_status(403)
|
||||||
|
|> put_view(EntenduWeb.ErrorView)
|
||||||
|
|> render("error_code.json", message: "Unauthorized", code: 403)
|
||||||
|
|> halt
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
conn
|
||||||
|
|> put_status(422)
|
||||||
|
|> put_view(EntenduWeb.ErrorView)
|
||||||
|
|> render("error_code.json", message: reason, code: 422)
|
||||||
|
|> halt
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_user_from_path(%{params: %{"path" => [_, link_id, _]}} = conn) do
|
||||||
|
get_session(conn, :current_user)
|
||||||
|
|> get_user_from_path()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_user_from_path(nil) do
|
||||||
|
{:error, "User not authenticated"}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_user_from_path(%{id: _, name: _, emails: _} = user) do
|
||||||
|
{:ok, user}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_user_from_path(_), do: {:error, "Link does not exist"}
|
||||||
|
end
|
|
@ -1,6 +1,8 @@
|
||||||
defmodule EntenduWeb.Router do
|
defmodule EntenduWeb.Router do
|
||||||
use EntenduWeb, :router
|
use EntenduWeb, :router
|
||||||
|
|
||||||
|
alias EntenduWeb.Plugs.AuthorizeLink
|
||||||
|
|
||||||
pipeline :browser do
|
pipeline :browser do
|
||||||
plug :accepts, ["html"]
|
plug :accepts, ["html"]
|
||||||
plug :fetch_session
|
plug :fetch_session
|
||||||
|
@ -14,6 +16,11 @@ defmodule EntenduWeb.Router do
|
||||||
plug :accepts, ["json"]
|
plug :accepts, ["json"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
pipeline :authorized_links do
|
||||||
|
plug AuthorizeLink
|
||||||
|
plug Plug.Static, at: "/uploads", from: Path.expand('./uploads'), gzip: false
|
||||||
|
end
|
||||||
|
|
||||||
scope "/", EntenduWeb do
|
scope "/", EntenduWeb do
|
||||||
pipe_through :browser
|
pipe_through :browser
|
||||||
|
|
||||||
|
@ -24,6 +31,8 @@ defmodule EntenduWeb.Router do
|
||||||
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
|
get "/just/for/you/:id", LinkController, :auth_page
|
||||||
|
get "/links/:id/text", LinkController, :text
|
||||||
|
get "/links/:id/file", LinkController, :file
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/auth", EntenduWeb do
|
scope "/auth", EntenduWeb do
|
||||||
|
@ -34,6 +43,11 @@ defmodule EntenduWeb.Router do
|
||||||
delete "/logout", AuthController, :delete
|
delete "/logout", AuthController, :delete
|
||||||
end
|
end
|
||||||
|
|
||||||
|
scope "/uploads", EntenduWeb do
|
||||||
|
pipe_through [:browser, :authorized_links]
|
||||||
|
get "/*path", FileNotFoundController, :index
|
||||||
|
end
|
||||||
|
|
||||||
# Other scopes may use custom stacks.
|
# Other scopes may use custom stacks.
|
||||||
# scope "/api", EntenduWeb do
|
# scope "/api", EntenduWeb do
|
||||||
# pipe_through :api
|
# pipe_through :api
|
||||||
|
|
|
@ -1 +1,7 @@
|
||||||
<%= react_component("Components.AuthPage", %{ csrf: Plug.CSRFProtection.get_csrf_token(), service: @service, recipient: @recipient, user: @current_user }) %>
|
<%= react_component("Components.AuthPage", %{
|
||||||
|
csrf: Plug.CSRFProtection.get_csrf_token(),
|
||||||
|
service: @intended_link.service,
|
||||||
|
recipient: @intended_link.recipient,
|
||||||
|
user: current_user(@conn),
|
||||||
|
link: current_link(@conn)
|
||||||
|
}) %>
|
||||||
|
|
|
@ -31,14 +31,14 @@ defmodule Entendu.EncryptedLink do
|
||||||
# end
|
# end
|
||||||
|
|
||||||
# Override the persisted filenames:
|
# Override the persisted filenames:
|
||||||
# def filename(version, _) do
|
def filename(_version, {_file, %{filename: filename}}) do
|
||||||
# version
|
if filename, do: filename, else: "text"
|
||||||
# end
|
end
|
||||||
|
|
||||||
# Override the storage directory:
|
# Override the storage directory:
|
||||||
# def storage_dir(version, {file, scope}) do
|
def storage_dir(version, {_file, scope}) do
|
||||||
# "uploads/user/avatars/#{scope.id}"
|
"uploads/links/#{scope.id}"
|
||||||
# end
|
end
|
||||||
|
|
||||||
# Provide a default URL if there hasn't been a file uploaded
|
# Provide a default URL if there hasn't been a file uploaded
|
||||||
# def default_url(version, scope) do
|
# def default_url(version, scope) do
|
||||||
|
|
|
@ -13,4 +13,13 @@ defmodule EntenduWeb.ErrorView do
|
||||||
def template_not_found(template, _assigns) do
|
def template_not_found(template, _assigns) do
|
||||||
Phoenix.Controller.status_message_from_template(template)
|
Phoenix.Controller.status_message_from_template(template)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def render("error_code.json", %{message: message} = params) do
|
||||||
|
code = Map.get(params, :code, "")
|
||||||
|
|
||||||
|
%{
|
||||||
|
message: message,
|
||||||
|
code: code
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue