13 Commits

25 changed files with 276 additions and 138 deletions

4
.gitignore vendored
View File

@@ -35,5 +35,5 @@ npm-debug.log
/priv/cert/ /priv/cert/
dev.secret.exs dev.secret.exs
/uploads/* priv/uploads/*
!/uploads/.gitkeep !priv/uploads/.gitkeep

View File

@@ -12,15 +12,15 @@
transition: opacity 1s ease-out; transition: opacity 1s ease-out;
} }
.phx-disconnected{ .phx-disconnected {
cursor: wait; cursor: wait;
} }
.phx-disconnected *{ .phx-disconnected * {
pointer-events: none; pointer-events: none;
} }
.phx-modal { .phx-modal {
opacity: 1!important; opacity: 1 !important;
position: fixed; position: fixed;
z-index: 1; z-index: 1;
left: 0; left: 0;
@@ -28,8 +28,8 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: auto; overflow: auto;
background-color: rgb(0,0,0); background-color: rgb(0, 0, 0);
background-color: rgba(0,0,0,0.4); background-color: rgba(0, 0, 0, 0.4);
} }
.phx-modal-content { .phx-modal-content {
@@ -54,7 +54,6 @@
cursor: pointer; cursor: pointer;
} }
/* Alerts and form errors */ /* Alerts and form errors */
.alert { .alert {
padding: 15px; padding: 15px;
@@ -88,3 +87,47 @@
display: block; display: block;
margin: -1rem 0 2rem; margin: -1rem 0 2rem;
} }
.logo {
width: 200px;
padding: 20px;
border-right: 1px #efefef solid;
}
.centered-container {
margin-top: 3rem;
background: none !important;
height: auto;
}
html {
height: 100%;
}
@media screen and (min-width: 1025px) {
.centered-container {
margin-top: 0px;
position: absolute;
top: 50%;
transform: translate(0, -50%);
}
}
@media screen and (max-width: 1024px) {
.logo {
display: block;
margin-left: auto;
margin-right: auto;
border-right: none;
border-bottom: 1px #efefef solid;
}
.splashHeader {
font-size: 1.9rem;
}
.splashSubheader {
font-size: 1.4rem;
font-weight: 200;
}
}

View File

@@ -24,6 +24,7 @@ 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 PrivacyPolicyPage from './pages/PrivacyPolicyPage';
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}})
@@ -43,5 +44,5 @@ liveSocket.connect()
window.liveSocket = liveSocket window.liveSocket = liveSocket
window.Components = { window.Components = {
SplashPage, JustPage, ForPage, YouPage, AuthPage SplashPage, JustPage, ForPage, YouPage, AuthPage, PrivacyPolicyPage
} }

View File

@@ -12,5 +12,7 @@ type OAuthEmail = {
type IntendedLink = { type IntendedLink = {
filename: string | null, filename: string | null,
filetype: string | null filetype: string | null,
text_content: string | null,
file_content: string | null
}; };

View File

@@ -40,12 +40,12 @@ const AuthPage = (props: AuthPageProps) => {
const [secretFileUrl, setSecretFileUrl] = useState<string>("#"); const [secretFileUrl, setSecretFileUrl] = useState<string>("#");
const [secretFileName, setSecretFileName] = useState<string>(""); const [secretFileName, setSecretFileName] = useState<string>("");
const [secretMessage, setSecretMessage] = useState<string>("Decrypting..."); const [secretMessage, setSecretMessage] = useState<string>("");
const [messageRevealed, setMessageRevealed] = useState<boolean>(false); const [messageRevealed, setMessageRevealed] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
init().catch((reason) => { init().catch((reason) => {
alert(reason); console.log(reason);
}); });
}, []); }, []);
@@ -55,7 +55,7 @@ const AuthPage = (props: AuthPageProps) => {
const init = async (): Promise<void> => { const init = async (): Promise<void> => {
const link: LinkFiles | null = await retrieveLink(); const link: LinkFiles | null = await retrieveLink();
const keys: Keys | null = await retrieveKeys(); const keys: Keys | null = await retrieveKeys();
if (link && keys) { if (link && keys && user) {
await decrypt(link, keys); await decrypt(link, keys);
} }
}; };
@@ -77,25 +77,36 @@ const AuthPage = (props: AuthPageProps) => {
} }
const linkResponse = await fetch(`/links/${linkId}`); const linkResponse = await fetch(`/links/${linkId}`);
const linkData: IntendedLink = await linkResponse.json(); let linkData: IntendedLink | null;
const textResponse = await fetch( let textData = null;
`/uploads/links/${linkId}/secret_message.txt` let fileData = null;
); if (linkResponse.status !== 200) {
const textData = await textResponse.blob(); throw new Error(linkResponse.statusText);
const fileResponse = await fetch( return null;
`/uploads/links/${linkId}/${linkData.filename}` }
); linkData = await linkResponse.json();
const fileData = await fileResponse.blob();
if (linkData) {
const textResponse = linkData.text_content
? await fetch(`/uploads/links/${linkId}/secret_message.txt`)
: null;
textData = textResponse ? await textResponse.blob() : null;
const fileResponse = linkData.file_content
? await fetch(`/uploads/links/${linkId}/${linkData.filename}`)
: null;
fileData = fileResponse ? await fileResponse.blob() : null;
if (linkData.filename) { if (linkData.filename) {
await setSecretFileName(linkData.filename); await setSecretFileName(linkData.filename);
} }
}
return { return {
text: textData.size > 0 ? textData : null, text: textData,
file: fileData.size > 0 ? fileData : null, file: fileData,
filename: linkData.filename, filename: linkData ? linkData.filename : null,
filetype: linkData.filetype, filetype: linkData ? linkData.filetype : null,
}; };
}; };
@@ -107,13 +118,13 @@ const AuthPage = (props: AuthPageProps) => {
fragmentData[0] = fragmentData[0].slice(1); fragmentData[0] = fragmentData[0].slice(1);
if (fragmentData.length <= 1) { if (fragmentData.length <= 1) {
key = sessionStorage.getItem("link_key"); key = sessionStorage.getItem("key_hex");
iv = sessionStorage.getItem("link_iv"); iv = sessionStorage.getItem("iv_hex");
} else { } else {
key = fragmentData[0]; key = fragmentData[0];
iv = fragmentData[1]; iv = fragmentData[1];
sessionStorage.setItem("link_key", key); sessionStorage.setItem("key_hex", key);
sessionStorage.setItem("link_iv", iv); sessionStorage.setItem("iv_hex", iv);
} }
if (key && iv) { if (key && iv) {
@@ -134,6 +145,7 @@ const AuthPage = (props: AuthPageProps) => {
true, true,
["encrypt", "decrypt"] ["encrypt", "decrypt"]
); );
if (link?.text) { if (link?.text) {
const textFile = await link.text.arrayBuffer(); const textFile = await link.text.arrayBuffer();
const encodedText = await window.crypto.subtle.decrypt( const encodedText = await window.crypto.subtle.decrypt(
@@ -201,7 +213,7 @@ const AuthPage = (props: AuthPageProps) => {
const renderHeader = (): JSX.Element => { const renderHeader = (): JSX.Element => {
return ( return (
<div> <div>
<Header2 style={{ margin: ".4rem" }}> <Header2 style={{ marginBottom: ".4rem" }}>
{user ? "You have been identified!" : "Someone sent you a secret"} {user ? "You have been identified!" : "Someone sent you a secret"}
</Header2> </Header2>
{user ? ( {user ? (
@@ -226,7 +238,7 @@ const AuthPage = (props: AuthPageProps) => {
const renderAuth = (): JSX.Element => { const renderAuth = (): JSX.Element => {
return ( return (
<CenteredContainer fullscreen> <CenteredContainer fullscreen className="centered-container">
<CenteredContainer> <CenteredContainer>
{renderHeader()} {renderHeader()}
<Spacer space="3rem" /> <Spacer space="3rem" />
@@ -256,7 +268,7 @@ const AuthPage = (props: AuthPageProps) => {
const renderReveal = (): JSX.Element => { const renderReveal = (): JSX.Element => {
return ( return (
<CenteredContainer fullscreen> <CenteredContainer fullscreen className="centered-container">
<CenteredContainer> <CenteredContainer>
{renderHeader()} {renderHeader()}
<Spacer space="3rem" /> <Spacer space="3rem" />

View File

@@ -1,11 +1,23 @@
import React, { useState } from "react"; 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";
type ForPageProps = { type ForPageProps = {
csrf: string csrf: string;
} };
const ForPage = (props: ForPageProps) => { const ForPage = (props: ForPageProps) => {
const [recipientInput, setRecipientInput] = useState(""); const [recipientInput, setRecipientInput] = useState("");
@@ -22,12 +34,6 @@ const ForPage = (props: ForPageProps) => {
}; };
const postContacts = async () => { const postContacts = async () => {
// const fragmentData = window.location.hash.split('.');
// if (fragmentData.length <= 0) {
// alert("No key found in fragment URI");
// return;
// }
const linkId = sessionStorage.getItem("link_id"); const linkId = sessionStorage.getItem("link_id");
if (!linkId) { if (!linkId) {
alert("No created link found in storage"); alert("No created link found in storage");
@@ -35,20 +41,20 @@ const ForPage = (props: ForPageProps) => {
} }
const formData = new FormData(); const formData = new FormData();
formData.append('recipient', recipientInput); formData.append("recipient", recipientInput);
formData.append('service', serviceSelect); formData.append("service", serviceSelect);
formData.append("link_id", linkId); formData.append("link_id", linkId);
try { try {
const results = await fetch(`${window.location.origin}/just/for`, { const results = await fetch(`${window.location.origin}/just/for`, {
headers: { headers: {
"X-CSRF-Token": props.csrf "X-CSRF-Token": props.csrf,
}, },
body: formData, body: formData,
method: "POST" method: "POST",
}); });
if (!results.ok) { if (!results.ok) {
throw new Error('Network response was not OK'); throw new Error("Network response was not OK");
} }
await results.json(); await results.json();
@@ -61,7 +67,7 @@ const ForPage = (props: ForPageProps) => {
return ( return (
<React.StrictMode> <React.StrictMode>
<GlobalStyle /> <GlobalStyle />
<CenteredContainer fullscreen> <CenteredContainer fullscreen className="centered-container">
<CenteredContainer> <CenteredContainer>
<ProgressIndicator currentProgress={2} /> <ProgressIndicator currentProgress={2} />
<Header2>Tell Someone</Header2> <Header2>Tell Someone</Header2>
@@ -79,7 +85,8 @@ const ForPage = (props: ForPageProps) => {
<Spacer space="2.5rem" /> <Spacer space="2.5rem" />
<TextAlignWrapper align="left"> <TextAlignWrapper align="left">
<Label htmlFor="serviceSelector"> <Label htmlFor="serviceSelector">
What type of account is the above username or email associated with? What type of account is the above username or email associated
with?
</Label> </Label>
</TextAlignWrapper> </TextAlignWrapper>
<Select <Select
@@ -87,11 +94,14 @@ const ForPage = (props: ForPageProps) => {
onChange={handleServiceChange} onChange={handleServiceChange}
value={serviceSelect} value={serviceSelect}
> >
<option value='github'>Github</option> <option value="github">Github</option>
</Select> </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")}
>
<IconArrow arrowDirection="left" /> <IconArrow arrowDirection="left" />
</Button> </Button>
<Button onClick={postContacts}>Generate Secret Code</Button> <Button onClick={postContacts}>Generate Secret Code</Button>

View File

@@ -74,7 +74,11 @@ const JustPage = (props: JustPageProps) => {
} }
}; };
const fileFormData = async (form: FormData, aesKey: AESKey) => { const fileFormData = async (
form: FormData,
aesKey: AESKey
): Promise<FormData> => {
if (!fileInput) return form;
const encoded = HexMix.stringToArrayBuffer(fileInput as string); const encoded = HexMix.stringToArrayBuffer(fileInput as string);
const encrypted = await window.crypto.subtle.encrypt( const encrypted = await window.crypto.subtle.encrypt(
{ {
@@ -91,9 +95,14 @@ const JustPage = (props: JustPageProps) => {
HexMix.arrayBufferToString(encrypted, (result: string) => { HexMix.arrayBufferToString(encrypted, (result: string) => {
sessionStorage.setItem("encoded_file", result); sessionStorage.setItem("encoded_file", result);
}); });
return form;
}; };
const textFormData = async (form: FormData, aesKey: AESKey) => { const textFormData = async (
form: FormData,
aesKey: AESKey
): Promise<FormData> => {
if (!secretInput) return form;
const encoded = HexMix.stringToArrayBuffer(secretInput); const encoded = HexMix.stringToArrayBuffer(secretInput);
const encrypted = await window.crypto.subtle.encrypt( const encrypted = await window.crypto.subtle.encrypt(
{ {
@@ -108,6 +117,7 @@ const JustPage = (props: JustPageProps) => {
HexMix.arrayBufferToString(encrypted, (result: string) => { HexMix.arrayBufferToString(encrypted, (result: string) => {
sessionStorage.setItem("encoded_message", result); sessionStorage.setItem("encoded_message", result);
}); });
return form;
}; };
const createKey = async (): Promise<AESKey> => { const createKey = async (): Promise<AESKey> => {
@@ -141,9 +151,9 @@ const JustPage = (props: JustPageProps) => {
} }
const key = await createKey(); const key = await createKey();
const formData = new FormData(); let formData = new FormData();
await fileFormData(formData, key); formData = await fileFormData(formData, key);
await textFormData(formData, key); formData = await textFormData(formData, key);
try { try {
const link: Response = await fetch(`${window.location.origin}/just`, { const link: Response = await fetch(`${window.location.origin}/just`, {
@@ -167,18 +177,18 @@ const JustPage = (props: JustPageProps) => {
return ( return (
<React.StrictMode> <React.StrictMode>
<GlobalStyle /> <GlobalStyle />
<CenteredContainer fullscreen> <CenteredContainer fullscreen className="centered-container">
<CenteredContainer> <CenteredContainer>
<ProgressIndicator currentProgress={1} /> <ProgressIndicator currentProgress={1} />
<Header2>Create a secret</Header2> <Header2>Create a secret</Header2>
<TextAlignWrapper align="left"> <TextAlignWrapper align="left">
<Label htmlFor="secretInput">Enter your secret here</Label> <Label htmlFor="secretInput">Enter a secret message</Label>
</TextAlignWrapper> </TextAlignWrapper>
<TextArea <TextArea
id="secretInput" id="secretInput"
value={secretInput} value={secretInput}
onChange={handleChange} onChange={handleChange}
placeholder="Tell me your secrets" placeholder="Only your intended recipient will see this message."
/> />
<Spacer space="2rem" /> <Spacer space="2rem" />
<TextAlignWrapper align="center"> <TextAlignWrapper align="center">

View File

@@ -0,0 +1,62 @@
import React from "react";
import {
CenteredContainer,
Header1,
Header3,
GlobalStyle,
} from "@intended/intended-ui";
const PrivacyPolicyPage = () => {
return (
<React.StrictMode>
<GlobalStyle />
<CenteredContainer
fullscreen
style={{
background: "none",
height: "auto",
}}
>
<CenteredContainer wide style={{ maxWidth: "800px" }}>
<Header1>Privacy Policy</Header1>
<Header3
small
style={{
color: "#CCCCCC",
textAlign: "left",
fontSize: "18px",
lineHeight: 1.6,
}}
>
<p>
This instance of Intended Link collects as little data as
necessary to provide its service. It can not read the secret
message and secret file, and all data associated with a link is
deleted once it expires.
</p>
<p>
Each link created will store the recipient's username or email and
its associated service for authorization purposes. It will store
the filename and filetype of the secret file, if it exists, to
make it easier for users to download and use the file once it's
decrypted. We store the timestamps of when the Link was created
and updated, along with when the link should expire.
</p>
<p>
When you authenticate with one of our supported OAuth providers,
we receive the third party's response, and if verification was
successful, we store the account's username and verified emails in
a short-lived session store. This data is then used to determine
whether the user is permitted to download the link's associated
secret message and file.
</p>
<p>This software is licensed under LGPL.</p>
</Header3>
</CenteredContainer>
</CenteredContainer>
</React.StrictMode>
);
};
export default PrivacyPolicyPage;

View File

@@ -28,13 +28,31 @@ const SplashPage = (props: SplashPageProps) => {
return ( return (
<React.StrictMode> <React.StrictMode>
<GlobalStyle /> <GlobalStyle />
<CenteredContainer fullscreen> <CenteredContainer
fullscreen
style={{
background: "none",
position: "absolute",
top: "50%",
transform: "translate(0, -50%)",
height: "auto",
}}
>
<CenteredContainer wide> <CenteredContainer wide>
<SplashIconHeader /> <SplashIconHeader style={{ width: "100%", maxWidth: "440px" }} />
<Header1>Securely Share Your Secrets</Header1> <Header1>
<span
className="splashHeader"
style={{ display: "block", marginTop: "20px" }}
>
Securely Share Your Secrets
</span>
</Header1>
<Header3> <Header3>
With Intended Link you can easily share messages and files securely <span className="splashSubheader">
and secretly. With Intended Link, you can send messages and files to any social
account in a secure and private manner.
</span>
</Header3> </Header3>
<Spacer /> <Spacer />
<Button <Button

View File

@@ -49,7 +49,7 @@ const YouPage = () => {
return ( return (
<React.StrictMode> <React.StrictMode>
<GlobalStyle /> <GlobalStyle />
<CenteredContainer fullscreen> <CenteredContainer fullscreen className="centered-container">
<CenteredContainer> <CenteredContainer>
<ProgressIndicator currentProgress={3} /> <ProgressIndicator currentProgress={3} />
<Header2>Share the secret</Header2> <Header2>Share the secret</Header2>

View File

@@ -1143,9 +1143,9 @@
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
}, },
"@intended/intended-ui": { "@intended/intended-ui": {
"version": "0.1.21", "version": "0.1.26",
"resolved": "https://registry.npmjs.org/@intended/intended-ui/-/intended-ui-0.1.21.tgz", "resolved": "https://registry.npmjs.org/@intended/intended-ui/-/intended-ui-0.1.26.tgz",
"integrity": "sha512-rNyJOGLOw8iKP0AaSYSytZXFssPhlE1FbUjxEze8F4iOgXxs6iu7B9+gSIF4QJg/IrQUVu76tkevBGDk2nTQNg==", "integrity": "sha512-+fSZctq4ywDUN6IJ+SbQyGhcx1fZmdyq/zsDqveJShFOGLOU39l5tT34/QHBScXlSLwgAkosCycldWyfW/olUg==",
"requires": { "requires": {
"polished": "^4.1.3", "polished": "^4.1.3",
"react": "^17.0.2", "react": "^17.0.2",
@@ -1720,14 +1720,15 @@
} }
}, },
"babel-plugin-styled-components": { "babel-plugin-styled-components": {
"version": "2.0.2", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.2.tgz", "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.5.tgz",
"integrity": "sha512-7eG5NE8rChnNTDxa6LQfynwgHTVOYYaHJbUYSlOhk8QBXIQiMBKq4gyfHBBKPrxUcVBXVJL61ihduCpCQbuNbw==", "integrity": "sha512-A7kfST5odbf8Ev42OQbj5teEiT8DskpRoQ/iPYePLLdcTCAsodpYKqtoy4SJthpsGzQKc2vvnrtlUgdmJq6WKQ==",
"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",
"babel-plugin-syntax-jsx": "^6.18.0", "babel-plugin-syntax-jsx": "^6.18.0",
"lodash": "^4.17.11" "lodash": "^4.17.11",
"picomatch": "^2.3.0"
} }
}, },
"babel-plugin-syntax-jsx": { "babel-plugin-syntax-jsx": {
@@ -5863,8 +5864,7 @@
"picomatch": { "picomatch": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
"integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw=="
"dev": true
}, },
"pify": { "pify": {
"version": "4.0.1", "version": "4.0.1",

View File

@@ -7,7 +7,7 @@
"watch": "webpack --mode development --watch" "watch": "webpack --mode development --watch"
}, },
"dependencies": { "dependencies": {
"@intended/intended-ui": "0.1.21", "@intended/intended-ui": "0.1.26",
"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",

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

View File

@@ -10,7 +10,7 @@ use Mix.Config
# which you should run after static files are built and # which you should run after static files are built and
# before starting your production server. # before starting your production server.
config :entendu, EntenduWeb.Endpoint, config :entendu, EntenduWeb.Endpoint,
url: [host: "example.com", port: 80], url: [host: "intended.link", port: 80],
cache_static_manifest: "priv/static/cache_manifest.json" cache_static_manifest: "priv/static/cache_manifest.json"
# Do not print debug messages in production # Do not print debug messages in production
@@ -45,8 +45,7 @@ config :logger, level: :info
# We also recommend setting `force_ssl` in your endpoint, ensuring # We also recommend setting `force_ssl` in your endpoint, ensuring
# no data is ever sent via http, always redirecting to https: # no data is ever sent via http, always redirecting to https:
# #
# config :entendu, EntenduWeb.Endpoint, config :my_app, EntenduWeb.Endpoint, force_ssl: [rewrite_on: [:x_forwarded_proto]]
# force_ssl: [hsts: true]
# #
# Check `Plug.SSL` for all available options in `force_ssl`. # Check `Plug.SSL` for all available options in `force_ssl`.

View File

@@ -28,8 +28,7 @@ defmodule EntenduWeb.AuthController do
link = get_session(conn, :intended_link) link = get_session(conn, :intended_link)
with %{id: link_id, recipient: recipient} <- link, with %{id: link_id, recipient: recipient} <- link,
{:ok, user} <- UserFromAuth.find_or_create(auth), {:ok, user} <- UserFromAuth.find_or_create(auth) do
true <- UserFromAuth.can_access?(recipient, user) do
# TODO: send over encrypted data that the frontend can decrypt # TODO: send over encrypted data that the frontend can decrypt
conn conn
@@ -42,11 +41,6 @@ defmodule EntenduWeb.AuthController do
|> put_flash(:error, "Could not find link to authenticate against") |> put_flash(:error, "Could not find link to authenticate against")
|> redirect(to: "/just/for/you/") |> redirect(to: "/just/for/you/")
false ->
conn
|> put_flash(:error, "#{link.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)

View File

@@ -36,6 +36,7 @@ defmodule EntenduWeb.LinkController 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
|> put_session(:intended_link, %{})
|> render("show_authorized.json", %{link: link}) |> render("show_authorized.json", %{link: link})
end end
end end
@@ -45,9 +46,9 @@ defmodule EntenduWeb.LinkController do
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{id: id, service: service, recipient: recipient} = link <- Links.get_link(link_id) do
conn conn
|> put_session(:intended_link, %{service: service, recipient: recipient}) |> put_session(:intended_link, %{id: id, service: service, recipient: recipient})
|> render("auth.html", %{intended_link: %{service: service, recipient: recipient}}) |> render("auth.html", %{intended_link: %{service: service, recipient: recipient}})
end end
end end

View File

@@ -6,6 +6,13 @@ defmodule EntenduWeb.PageController do
use EntenduWeb, :controller use EntenduWeb, :controller
def index(conn, _params) do def index(conn, _params) do
render(conn, "index.html", current_user: get_session(conn, :current_user)) conn
|> clear_session()
|> render("index.html")
end
def privacy(conn, _params) do
conn
|> render("privacy_policy.html")
end end
end end

View File

@@ -18,7 +18,7 @@ defmodule EntenduWeb.Router do
pipeline :authorized_files do pipeline :authorized_files do
plug AuthorizeLink plug AuthorizeLink
plug Plug.Static, at: "/uploads", from: Path.expand('./uploads'), gzip: false plug Plug.Static, at: "/uploads", from: {:entendu, "priv/uploads"}, gzip: false
end end
pipeline :authorized_link do pipeline :authorized_link do
@@ -35,6 +35,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 "/privacy-policy", PageController, :privacy
end end
scope "/auth", EntenduWeb do scope "/auth", EntenduWeb do

View File

@@ -1,3 +1,6 @@
<main role="main"> <main role="main">
<a href="/">
<img src="<%= Routes.static_path(@conn, "/images/logo.png") %>" class="logo" />
</a>
<%= @inner_content %> <%= @inner_content %>
</main> </main>

View File

@@ -4,12 +4,24 @@
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<!-- Essential META Tags -->
<meta property="og:title" content="Intended Link">
<meta property="og:type" content="website" />
<meta property="og:image" content="https://intended.link/images/thumbnail.jpg">
<meta property="og:url" content="https://intended.link">
<meta name="twitter:card" content="summary_large_image">
<!-- Non-Essential, But Recommended -->
<meta property="og:description" content="Securely send private messages to social media accounts.">
<meta property="og:site_name" content="Intended Link">
<meta name="twitter:image:alt" content="Preview of splash page">
<%= csrf_meta_tag() %> <%= csrf_meta_tag() %>
<%= live_title_tag assigns[:page_title] || "Entendu", suffix: " · Phoenix Framework" %> <%= live_title_tag assigns[:page_title] || "Intended Link", suffix: "" %>
<link phx-track-static rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/> <link phx-track-static rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
<script defer phx-track-static type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script> <script defer phx-track-static type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
</head> </head>
<body style="background: #060b2e;"> <body style="background: linear-gradient(180deg,#060b2e 0%,#051745 100%); min-height: 100%;">
<%= @inner_content %> <%= @inner_content %>
</body> </body>
</html> </html>

View File

@@ -2,45 +2,4 @@
<%= react_component("Components.SplashPage", %{ error: get_flash(@conn, :error) }) %> <%= react_component("Components.SplashPage", %{ error: get_flash(@conn, :error) }) %>
<%= if @current_user do %>
<h2>Welcome, <%= @current_user.name %>!</h2>
<div>
<img src="<%= @current_user.avatar %>" />
</div>
<%= link "Logout", to: Routes.auth_path(@conn, :delete), method: "delete", class: "button" %>
<br>
<% else %>
<ul style="display: none;">
<li>
<a class="button" href="<%= Routes.auth_path(@conn, :request, "github") %>">
<i class="fa fa-github"></i>
Sign in with GitHub
</a>
</li>
<li>
<a class="button" href="<%= Routes.auth_path(@conn, :request, "facebook") %>">
<i class="fa fa-facebook"></i>
Sign in with Facebook
</a>
</li>
<li>
<a class="button" href="<%= Routes.auth_path(@conn, :request, "google") %>">
<i class="fa fa-google"></i>
Sign in with Google
</a>
</li>
<li>
<a class="button" href="<%= Routes.auth_path(@conn, :request, "slack") %>">
<i class="fa fa-slack"></i>
Sign in with Slack
</a>
</li>
<li>
<a class="button" href="<%= Routes.auth_path(@conn, :request, "twitter") %>">
<i class="fa fa-twitter"></i>
Sign in with Twitter
</a>
</li>
</ul>
<% end %>
</section> </section>

View File

@@ -0,0 +1,3 @@
<section>
<%= react_component("Components.PrivacyPolicyPage") %>
</section>

View File

@@ -37,7 +37,7 @@ defmodule Entendu.EncryptedLink do
# Override the storage directory: # Override the storage directory:
def storage_dir(version, {_file, scope}) do def storage_dir(version, {_file, scope}) do
"uploads/links/#{scope.id}" "priv/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