15 Commits

Author SHA1 Message Date
beb6c35de9 fix site title and add social meta tags 2022-02-25 01:19:02 -05:00
5132f41cad Merge pull request 'feature/add-privacy-policy' (#23) from feature/add-privacy-policy into master
Reviewed-on: #23
2022-02-25 05:50:19 +00:00
9b362d0241 add a barebones privacy policy, will likely need to expand it if the service gets more usage 2022-02-25 00:48:40 -05:00
7e2eb2cb75 Merge pull request 'hotfix/desktop-cant-click-logo' (#22) from hotfix/desktop-cant-click-logo into master
Reviewed-on: #22
2022-02-25 04:07:08 +00:00
67b307612d let splash container not sit over logo, and also testing merge commit signing 2022-02-24 22:18:58 -05:00
3f7da22499 fix bug where logo isn't clickable on splash screen 2022-02-24 11:41:29 -05:00
50012ad7b3 better desktop styling, change some wording 2022-02-24 11:32:00 -05:00
1696995fb6 typo 2022-02-24 02:10:37 -05:00
04c96fafc4 change local file upload location 2022-02-24 02:09:40 -05:00
38a7ac597b force ssl 2022-02-24 01:41:14 -05:00
0847b0ff3d fix margin on splash page 2022-02-24 01:19:58 -05:00
2ccb3d0053 bug fixes, add logo, mobile styling 2022-02-24 01:15:44 -05:00
32cefda0a0 Merge pull request 'feature/properly-display-emails' (#13) from feature/properly-display-emails into master
Reviewed-on: #13
2022-02-22 22:34:02 +00:00
3c9dd96d8b properly display username/emails of logged in user, and add logout button 2022-02-22 17:32:45 -05:00
8330bb420e properly display encrypted text 2022-02-22 03:05:29 -05:00
26 changed files with 424 additions and 168 deletions

4
.gitignore vendored
View File

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

View File

@@ -12,15 +12,15 @@
transition: opacity 1s ease-out;
}
.phx-disconnected{
.phx-disconnected {
cursor: wait;
}
.phx-disconnected *{
.phx-disconnected * {
pointer-events: none;
}
.phx-modal {
opacity: 1!important;
opacity: 1 !important;
position: fixed;
z-index: 1;
left: 0;
@@ -28,8 +28,8 @@
width: 100%;
height: 100%;
overflow: auto;
background-color: rgb(0,0,0);
background-color: rgba(0,0,0,0.4);
background-color: rgb(0, 0, 0);
background-color: rgba(0, 0, 0, 0.4);
}
.phx-modal-content {
@@ -54,7 +54,6 @@
cursor: pointer;
}
/* Alerts and form errors */
.alert {
padding: 15px;
@@ -88,3 +87,47 @@
display: block;
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 YouPage from './pages/YouPage';
import AuthPage from './pages/AuthPage';
import PrivacyPolicyPage from './pages/PrivacyPolicyPage';
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})
@@ -43,5 +44,5 @@ liveSocket.connect()
window.liveSocket = liveSocket
window.Components = {
SplashPage, JustPage, ForPage, YouPage, AuthPage
SplashPage, JustPage, ForPage, YouPage, AuthPage, PrivacyPolicyPage
}

View File

@@ -1,9 +1,18 @@
type IntendedUser = {
name: string;
emails: string[];
emails: OAuthEmail[];
username: string;
};
type OAuthEmail = {
email: string;
primary: boolean;
verified: boolean;
};
type IntendedLink = {
filename: string | null,
filetype: string | null
filetype: string | null,
text_content: string | null,
file_content: string | null
};

View File

@@ -20,6 +20,7 @@ type AuthPageProps = {
service: string;
recipient: string;
user: IntendedUser | null;
error: string;
};
interface Keys {
@@ -39,22 +40,34 @@ const AuthPage = (props: AuthPageProps) => {
const [secretFileUrl, setSecretFileUrl] = useState<string>("#");
const [secretFileName, setSecretFileName] = useState<string>("");
const [secretMessage, setSecretMessage] = useState<string>("Decrypting...");
const [secretMessage, setSecretMessage] = useState<string>("");
const [messageRevealed, setMessageRevealed] = useState<boolean>(false);
useEffect(() => {
init().catch((reason) => {
alert(reason);
console.log(reason);
});
}, []);
const capitalize = (s: string) =>
(s && s[0].toUpperCase() + s.slice(1)) || "";
const init = async (): Promise<void> => {
const link: LinkFiles | null = await retrieveLink();
const keys: Keys | null = await retrieveKeys();
if (link && keys) {
if (link && keys && user) {
await decrypt(link, keys);
}
};
const userEmails = (): string[] => {
return user
? user.emails
.filter((email) => email.verified)
.map((email) => email.email)
: [];
};
const retrieveLink = async (): Promise<LinkFiles | null> => {
const urlSegments = new URL(document.URL).pathname.split("/");
const linkId = urlSegments.pop() || urlSegments.pop();
@@ -64,25 +77,36 @@ const AuthPage = (props: AuthPageProps) => {
}
const linkResponse = await fetch(`/links/${linkId}`);
const linkData: IntendedLink = await linkResponse.json();
const textResponse = await fetch(
`/uploads/links/${linkId}/secret_message.txt`
);
const textData = await textResponse.blob();
const fileResponse = await fetch(
`/uploads/links/${linkId}/${linkData.filename}`
);
const fileData = await fileResponse.blob();
let linkData: IntendedLink | null;
let textData = null;
let fileData = null;
if (linkResponse.status !== 200) {
throw new Error(linkResponse.statusText);
return null;
}
linkData = await linkResponse.json();
if (linkData.filename) {
await setSecretFileName(linkData.filename);
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) {
await setSecretFileName(linkData.filename);
}
}
return {
text: textData.size > 0 ? textData : null,
file: fileData.size > 0 ? fileData : null,
filename: linkData.filename,
filetype: linkData.filetype,
text: textData,
file: fileData,
filename: linkData ? linkData.filename : null,
filetype: linkData ? linkData.filetype : null,
};
};
@@ -94,13 +118,13 @@ const AuthPage = (props: AuthPageProps) => {
fragmentData[0] = fragmentData[0].slice(1);
if (fragmentData.length <= 1) {
key = sessionStorage.getItem("link_key");
iv = sessionStorage.getItem("link_iv");
key = sessionStorage.getItem("key_hex");
iv = sessionStorage.getItem("iv_hex");
} else {
key = fragmentData[0];
iv = fragmentData[1];
sessionStorage.setItem("link_key", key);
sessionStorage.setItem("link_iv", iv);
sessionStorage.setItem("key_hex", key);
sessionStorage.setItem("iv_hex", iv);
}
if (key && iv) {
@@ -121,6 +145,7 @@ const AuthPage = (props: AuthPageProps) => {
true,
["encrypt", "decrypt"]
);
if (link?.text) {
const textFile = await link.text.arrayBuffer();
const encodedText = await window.crypto.subtle.decrypt(
@@ -135,6 +160,7 @@ const AuthPage = (props: AuthPageProps) => {
// And voila
HexMix.arrayBufferToString(encodedText, (result: string) => {
setSecretMessage(result);
setMessageRevealed(true);
});
}
if (link?.file) {
@@ -153,21 +179,57 @@ const AuthPage = (props: AuthPageProps) => {
type: link.filetype ? link.filetype : "text/plain",
});
setSecretFileUrl(window.URL.createObjectURL(blob));
setMessageRevealed(true);
}
};
const renderFooter = (): JSX.Element => {
if (!user) return <div></div>;
return (
<Header3
small
style={{ color: "#CCCCCC", fontSize: "1.4rem", textAlign: "left" }}
>
Hello {user.name}, you are logged in to{" "}
<span style={{ color: "#A849CF" }}>{capitalize(service)}</span> as{" "}
<span style={{ color: "#32EFE7" }}>{user.username}</span>. This account
has the following emails associated with it:
<br />
<br />
<span style={{ color: "#32EFE7" }}>{userEmails().join(", ")}</span>
<br />
<br />
The intended recipient for this message is{" "}
<span style={{ color: "#32EFE7" }}>{recipient}</span> on{" "}
<span style={{ color: "#A849CF" }}>{capitalize(service)}</span>. If you
need to authenticate with a different account, you may do so by logging
out and accessing this link again. It's also possible that you have yet
to verify your email address on{" "}
<span style={{ color: "#A849CF" }}>{capitalize(service)}</span>.
</Header3>
);
};
const renderHeader = (): JSX.Element => {
return (
<div>
<Header2 style={{ margin: ".4rem" }}>Someone sent you a secret</Header2>
<Header2 style={{ marginBottom: ".4rem" }}>
{user ? "You have been identified!" : "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(", ")}
{messageRevealed
? "The following message and/or file is for your eyes only."
: "Unfortunately, you are not the intended recipient."}
<br />
<br />
</Header3>
) : (
<Header3 small>
Please verify your identity to reveal this message.
The intended recipient for this message is {recipient} on{" "}
{capitalize(service)}. Please verify your identity to reveal this
message.
</Header3>
)}
</div>
@@ -176,7 +238,7 @@ const AuthPage = (props: AuthPageProps) => {
const renderAuth = (): JSX.Element => {
return (
<CenteredContainer fullscreen>
<CenteredContainer fullscreen className="centered-container">
<CenteredContainer>
{renderHeader()}
<Spacer space="3rem" />
@@ -206,7 +268,7 @@ const AuthPage = (props: AuthPageProps) => {
const renderReveal = (): JSX.Element => {
return (
<CenteredContainer fullscreen>
<CenteredContainer fullscreen className="centered-container">
<CenteredContainer>
{renderHeader()}
<Spacer space="3rem" />
@@ -232,11 +294,13 @@ const AuthPage = (props: AuthPageProps) => {
/>
</a>
<Spacer space="3rem" />
<a href={`https://intended.link/auth/${service}`}>
<a href={`https://intended.link/auth/logout`}>
<Button variant="primary" wide onClick={() => {}}>
Re-Verify
Logout
</Button>
</a>
<Spacer space="3rem" />
{renderFooter()}
</CenteredContainer>
</CenteredContainer>
);

View File

@@ -1,11 +1,23 @@
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 = {
csrf: string
}
csrf: string;
};
const ForPage = (props: ForPageProps) => {
const [recipientInput, setRecipientInput] = useState("");
@@ -22,12 +34,6 @@ const ForPage = (props: ForPageProps) => {
};
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");
if (!linkId) {
alert("No created link found in storage");
@@ -35,20 +41,20 @@ const ForPage = (props: ForPageProps) => {
}
const formData = new FormData();
formData.append('recipient', recipientInput);
formData.append('service', serviceSelect);
formData.append("recipient", recipientInput);
formData.append("service", serviceSelect);
formData.append("link_id", linkId);
try {
const results = await fetch(`${window.location.origin}/just/for`, {
headers: {
"X-CSRF-Token": props.csrf
"X-CSRF-Token": props.csrf,
},
body: formData,
method: "POST"
method: "POST",
});
if (!results.ok) {
throw new Error('Network response was not OK');
throw new Error("Network response was not OK");
}
await results.json();
@@ -61,7 +67,7 @@ const ForPage = (props: ForPageProps) => {
return (
<React.StrictMode>
<GlobalStyle />
<CenteredContainer fullscreen>
<CenteredContainer fullscreen className="centered-container">
<CenteredContainer>
<ProgressIndicator currentProgress={2} />
<Header2>Tell Someone</Header2>
@@ -79,7 +85,8 @@ const ForPage = (props: ForPageProps) => {
<Spacer space="2.5rem" />
<TextAlignWrapper align="left">
<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>
</TextAlignWrapper>
<Select
@@ -87,11 +94,14 @@ const ForPage = (props: ForPageProps) => {
onChange={handleServiceChange}
value={serviceSelect}
>
<option value='github'>Github</option>
</Select>
<option value="github">Github</option>
</Select>
<Spacer space="3rem" />
<SpaceBetweenContainer>
<Button variant="secondary" onClick={() => window.location.href = "/just"}>
<Button
variant="secondary"
onClick={() => (window.location.href = "/just")}
>
<IconArrow arrowDirection="left" />
</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 encrypted = await window.crypto.subtle.encrypt(
{
@@ -88,9 +92,17 @@ const JustPage = (props: JustPageProps) => {
form.append("file_content", blobData, fileName);
form.append("filename", fileName);
form.append("filetype", fileType);
HexMix.arrayBufferToString(encrypted, (result: string) => {
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 encrypted = await window.crypto.subtle.encrypt(
{
@@ -102,6 +114,10 @@ const JustPage = (props: JustPageProps) => {
);
const blobData = new Blob([encrypted]);
form.append("text_content", blobData, "secret_message.txt");
HexMix.arrayBufferToString(encrypted, (result: string) => {
sessionStorage.setItem("encoded_message", result);
});
return form;
};
const createKey = async (): Promise<AESKey> => {
@@ -135,9 +151,9 @@ const JustPage = (props: JustPageProps) => {
}
const key = await createKey();
const formData = new FormData();
await fileFormData(formData, key);
await textFormData(formData, key);
let formData = new FormData();
formData = await fileFormData(formData, key);
formData = await textFormData(formData, key);
try {
const link: Response = await fetch(`${window.location.origin}/just`, {
@@ -161,18 +177,18 @@ const JustPage = (props: JustPageProps) => {
return (
<React.StrictMode>
<GlobalStyle />
<CenteredContainer fullscreen>
<CenteredContainer fullscreen className="centered-container">
<CenteredContainer>
<ProgressIndicator currentProgress={1} />
<Header2>Create a secret</Header2>
<TextAlignWrapper align="left">
<Label htmlFor="secretInput">Enter your secret here</Label>
<Label htmlFor="secretInput">Enter a secret message</Label>
</TextAlignWrapper>
<TextArea
id="secretInput"
value={secretInput}
onChange={handleChange}
placeholder="Tell me your secrets"
placeholder="Only your intended recipient will see this message."
/>
<Spacer space="2rem" />
<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

@@ -1,21 +1,65 @@
import React from "react";
import React, { useEffect } from "react";
import { CenteredContainer, SplashIconHeader, Header1, Header3, Spacer, Button, GlobalStyle } from '@intended/intended-ui';
import {
CenteredContainer,
SplashIconHeader,
Header1,
Header3,
Spacer,
Button,
GlobalStyle,
} from "@intended/intended-ui";
type SplashPageProps = {
error: string;
};
const SplashPage = (props: SplashPageProps) => {
useEffect(() => {
displayErrors();
});
const displayErrors = () => {
const { error } = props;
if (error) alert(error);
};
const SplashPage = () => {
return (
<React.StrictMode>
<GlobalStyle />
<CenteredContainer fullscreen>
<CenteredContainer
fullscreen
style={{
background: "none",
position: "absolute",
top: "50%",
transform: "translate(0, -50%)",
height: "auto",
}}
>
<CenteredContainer wide>
<SplashIconHeader />
<Header1>Securely Share Your Secrets</Header1>
<SplashIconHeader style={{ width: "100%", maxWidth: "440px" }} />
<Header1>
<span
className="splashHeader"
style={{ display: "block", marginTop: "20px" }}
>
Securely Share Your Secrets
</span>
</Header1>
<Header3>
With Intended Link you can easily share messages and files securely
and secretly.
<span className="splashSubheader">
With Intended Link, you can send messages and files to any social
account in a secure and private manner.
</span>
</Header3>
<Spacer />
<Button variant="secondary" boldFont onClick={() => window.location.href = "/just"}>
<Button
variant="secondary"
boldFont
onClick={() => (window.location.href = "/just")}
>
START SHARING
</Button>
</CenteredContainer>

View File

@@ -1,8 +1,29 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { ProgressIndicator, Header2, Button, IconArrow, InputButtonWithIcon, Label, Input, CenteredContainer, SpaceBetweenContainer, Spacer, TextAlignWrapper, GlobalStyle } from "@intended/intended-ui";
import {
ProgressIndicator,
Header2,
Button,
IconArrow,
InputButtonWithIcon,
Label,
Input,
CenteredContainer,
SpaceBetweenContainer,
Spacer,
TextAlignWrapper,
GlobalStyle,
} from "@intended/intended-ui";
const YouPage = () => {
const [url, setUrl] = useState("#");
const [encoded, setEncoded] = useState("");
useEffect(() => {
setUrl(calculateUrl());
setEncoded(calculateEncoded());
}, []);
const calculateUrl = () => {
const linkId = sessionStorage.getItem("link_id");
const keyHex = sessionStorage.getItem("key_hex");
@@ -11,7 +32,11 @@ const YouPage = () => {
return `${window.location.origin}/just/for/you/${linkId}#${keyHex}.${ivHex}`;
};
const [url, _setUrl] = useState(calculateUrl());
const calculateEncoded = () => {
const encodedFile = sessionStorage.getItem("encoded_file");
const encodedMessage = sessionStorage.getItem("encoded_message");
return `${encodedMessage}${encodedFile}`;
};
const copyUrl = async () => {
try {
@@ -19,12 +44,12 @@ const YouPage = () => {
} catch (err: any) {
alert("Could not copy url to clipboard.");
}
}
};
return (
<React.StrictMode>
<GlobalStyle />
<CenteredContainer fullscreen>
<CenteredContainer fullscreen className="centered-container">
<CenteredContainer>
<ProgressIndicator currentProgress={3} />
<Header2>Share the secret</Header2>
@@ -47,20 +72,23 @@ const YouPage = () => {
looking eh?:
</Label>
</TextAlignWrapper>
<Input
variant="disabled-light"
id="encodedSecret"
value={url}
/>
<Input variant="disabled-light" id="encodedSecret" value={encoded} />
<Spacer space="3rem" />
<SpaceBetweenContainer>
<Button variant="secondary" onClick={() => window.location.href = "/just/for"}>
<Button
variant="secondary"
onClick={() => (window.location.href = "/just/for")}
>
<IconArrow arrowDirection="left" />
</Button>
<Button onClick={() => {
sessionStorage.clear();
window.location.href = "/just";
}}>Create Another Secret</Button>
<Button
onClick={() => {
sessionStorage.clear();
window.location.href = "/just";
}}
>
Create Another Secret
</Button>
</SpaceBetweenContainer>
</CenteredContainer>
</CenteredContainer>

View File

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

View File

@@ -7,7 +7,7 @@
"watch": "webpack --mode development --watch"
},
"dependencies": {
"@intended/intended-ui": "0.1.21",
"@intended/intended-ui": "0.1.26",
"phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html",
"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
# before starting your production server.
config :entendu, EntenduWeb.Endpoint,
url: [host: "example.com", port: 80],
url: [host: "intended.link", port: 80],
cache_static_manifest: "priv/static/cache_manifest.json"
# 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
# no data is ever sent via http, always redirecting to https:
#
# config :entendu, EntenduWeb.Endpoint,
# force_ssl: [hsts: true]
config :my_app, EntenduWeb.Endpoint, force_ssl: [rewrite_on: [:x_forwarded_proto]]
#
# 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)
with %{id: link_id, recipient: recipient} <- link,
{:ok, user} <- UserFromAuth.find_or_create(auth),
true <- UserFromAuth.can_access?(recipient, user) do
{:ok, user} <- UserFromAuth.find_or_create(auth) do
# TODO: send over encrypted data that the frontend can decrypt
conn
@@ -42,11 +41,6 @@ defmodule EntenduWeb.AuthController do
|> put_flash(:error, "Could not find link to authenticate against")
|> 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} ->
conn
|> put_flash(:error, reason)

View File

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

View File

@@ -6,6 +6,13 @@ defmodule EntenduWeb.PageController do
use EntenduWeb, :controller
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

View File

@@ -18,7 +18,7 @@ defmodule EntenduWeb.Router do
pipeline :authorized_files do
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
pipeline :authorized_link do
@@ -35,14 +35,16 @@ defmodule EntenduWeb.Router do
post "/just/for", LinkController, :for
get "/just/for/you", LinkController, :you_page
get "/just/for/you/:id", LinkController, :auth_page
get "/privacy-policy", PageController, :privacy
end
scope "/auth", EntenduWeb do
pipe_through :browser
get "/logout", AuthController, :delete
get "/:provider", AuthController, :request
get "/:provider/callback", AuthController, :callback
delete "/logout", AuthController, :delete
end
scope "/uploads", EntenduWeb do

View File

@@ -1,5 +1,6 @@
<main role="main">
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
<a href="/">
<img src="<%= Routes.static_path(@conn, "/images/logo.png") %>" class="logo" />
</a>
<%= @inner_content %>
</main>

View File

@@ -4,12 +4,24 @@
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<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() %>
<%= 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") %>"/>
<script defer phx-track-static type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
</head>
<body style="background: #060b2e;">
<body style="background: linear-gradient(180deg,#060b2e 0%,#051745 100%); min-height: 100%;">
<%= @inner_content %>
</body>
</html>

View File

@@ -3,5 +3,6 @@
service: @intended_link.service,
recipient: @intended_link.recipient,
user: current_user(@conn),
link: current_link(@conn)
link: current_link(@conn),
error: get_flash(@conn, :error)
}) %>

View File

@@ -1,46 +1,5 @@
<section>
<%= react_component("Components.SplashPage") %>
<%= 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>

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:
def storage_dir(version, {_file, scope}) do
"uploads/links/#{scope.id}"
"priv/uploads/links/#{scope.id}"
end
# Provide a default URL if there hasn't been a file uploaded