Compare commits
No commits in common. "master" and "feature/support-for-usernames" have entirely different histories.
master
...
feature/su
|
@ -35,5 +35,5 @@ npm-debug.log
|
||||||
/priv/cert/
|
/priv/cert/
|
||||||
|
|
||||||
dev.secret.exs
|
dev.secret.exs
|
||||||
priv/uploads/*
|
/uploads/*
|
||||||
!priv/uploads/.gitkeep
|
!/uploads/.gitkeep
|
157
LICENSE.md
157
LICENSE.md
|
@ -1,157 +0,0 @@
|
||||||
### GNU LESSER GENERAL PUBLIC LICENSE
|
|
||||||
|
|
||||||
Version 3, 29 June 2007
|
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc.
|
|
||||||
<https://fsf.org/>
|
|
||||||
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies of this
|
|
||||||
license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
This version of the GNU Lesser General Public License incorporates the
|
|
||||||
terms and conditions of version 3 of the GNU General Public License,
|
|
||||||
supplemented by the additional permissions listed below.
|
|
||||||
|
|
||||||
#### 0. Additional Definitions.
|
|
||||||
|
|
||||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
|
||||||
General Public License, and the "GNU GPL" refers to version 3 of the
|
|
||||||
GNU General Public License.
|
|
||||||
|
|
||||||
"The Library" refers to a covered work governed by this License, other
|
|
||||||
than an Application or a Combined Work as defined below.
|
|
||||||
|
|
||||||
An "Application" is any work that makes use of an interface provided
|
|
||||||
by the Library, but which is not otherwise based on the Library.
|
|
||||||
Defining a subclass of a class defined by the Library is deemed a mode
|
|
||||||
of using an interface provided by the Library.
|
|
||||||
|
|
||||||
A "Combined Work" is a work produced by combining or linking an
|
|
||||||
Application with the Library. The particular version of the Library
|
|
||||||
with which the Combined Work was made is also called the "Linked
|
|
||||||
Version".
|
|
||||||
|
|
||||||
The "Minimal Corresponding Source" for a Combined Work means the
|
|
||||||
Corresponding Source for the Combined Work, excluding any source code
|
|
||||||
for portions of the Combined Work that, considered in isolation, are
|
|
||||||
based on the Application, and not on the Linked Version.
|
|
||||||
|
|
||||||
The "Corresponding Application Code" for a Combined Work means the
|
|
||||||
object code and/or source code for the Application, including any data
|
|
||||||
and utility programs needed for reproducing the Combined Work from the
|
|
||||||
Application, but excluding the System Libraries of the Combined Work.
|
|
||||||
|
|
||||||
#### 1. Exception to Section 3 of the GNU GPL.
|
|
||||||
|
|
||||||
You may convey a covered work under sections 3 and 4 of this License
|
|
||||||
without being bound by section 3 of the GNU GPL.
|
|
||||||
|
|
||||||
#### 2. Conveying Modified Versions.
|
|
||||||
|
|
||||||
If you modify a copy of the Library, and, in your modifications, a
|
|
||||||
facility refers to a function or data to be supplied by an Application
|
|
||||||
that uses the facility (other than as an argument passed when the
|
|
||||||
facility is invoked), then you may convey a copy of the modified
|
|
||||||
version:
|
|
||||||
|
|
||||||
- a) under this License, provided that you make a good faith effort
|
|
||||||
to ensure that, in the event an Application does not supply the
|
|
||||||
function or data, the facility still operates, and performs
|
|
||||||
whatever part of its purpose remains meaningful, or
|
|
||||||
- b) under the GNU GPL, with none of the additional permissions of
|
|
||||||
this License applicable to that copy.
|
|
||||||
|
|
||||||
#### 3. Object Code Incorporating Material from Library Header Files.
|
|
||||||
|
|
||||||
The object code form of an Application may incorporate material from a
|
|
||||||
header file that is part of the Library. You may convey such object
|
|
||||||
code under terms of your choice, provided that, if the incorporated
|
|
||||||
material is not limited to numerical parameters, data structure
|
|
||||||
layouts and accessors, or small macros, inline functions and templates
|
|
||||||
(ten or fewer lines in length), you do both of the following:
|
|
||||||
|
|
||||||
- a) Give prominent notice with each copy of the object code that
|
|
||||||
the Library is used in it and that the Library and its use are
|
|
||||||
covered by this License.
|
|
||||||
- b) Accompany the object code with a copy of the GNU GPL and this
|
|
||||||
license document.
|
|
||||||
|
|
||||||
#### 4. Combined Works.
|
|
||||||
|
|
||||||
You may convey a Combined Work under terms of your choice that, taken
|
|
||||||
together, effectively do not restrict modification of the portions of
|
|
||||||
the Library contained in the Combined Work and reverse engineering for
|
|
||||||
debugging such modifications, if you also do each of the following:
|
|
||||||
|
|
||||||
- a) Give prominent notice with each copy of the Combined Work that
|
|
||||||
the Library is used in it and that the Library and its use are
|
|
||||||
covered by this License.
|
|
||||||
- b) Accompany the Combined Work with a copy of the GNU GPL and this
|
|
||||||
license document.
|
|
||||||
- c) For a Combined Work that displays copyright notices during
|
|
||||||
execution, include the copyright notice for the Library among
|
|
||||||
these notices, as well as a reference directing the user to the
|
|
||||||
copies of the GNU GPL and this license document.
|
|
||||||
- d) Do one of the following:
|
|
||||||
- 0. Convey the Minimal Corresponding Source under the terms of
|
|
||||||
this License, and the Corresponding Application Code in a form
|
|
||||||
suitable for, and under terms that permit, the user to
|
|
||||||
recombine or relink the Application with a modified version of
|
|
||||||
the Linked Version to produce a modified Combined Work, in the
|
|
||||||
manner specified by section 6 of the GNU GPL for conveying
|
|
||||||
Corresponding Source.
|
|
||||||
- 1. Use a suitable shared library mechanism for linking with
|
|
||||||
the Library. A suitable mechanism is one that (a) uses at run
|
|
||||||
time a copy of the Library already present on the user's
|
|
||||||
computer system, and (b) will operate properly with a modified
|
|
||||||
version of the Library that is interface-compatible with the
|
|
||||||
Linked Version.
|
|
||||||
- e) Provide Installation Information, but only if you would
|
|
||||||
otherwise be required to provide such information under section 6
|
|
||||||
of the GNU GPL, and only to the extent that such information is
|
|
||||||
necessary to install and execute a modified version of the
|
|
||||||
Combined Work produced by recombining or relinking the Application
|
|
||||||
with a modified version of the Linked Version. (If you use option
|
|
||||||
4d0, the Installation Information must accompany the Minimal
|
|
||||||
Corresponding Source and Corresponding Application Code. If you
|
|
||||||
use option 4d1, you must provide the Installation Information in
|
|
||||||
the manner specified by section 6 of the GNU GPL for conveying
|
|
||||||
Corresponding Source.)
|
|
||||||
|
|
||||||
#### 5. Combined Libraries.
|
|
||||||
|
|
||||||
You may place library facilities that are a work based on the Library
|
|
||||||
side by side in a single library together with other library
|
|
||||||
facilities that are not Applications and are not covered by this
|
|
||||||
License, and convey such a combined library under terms of your
|
|
||||||
choice, if you do both of the following:
|
|
||||||
|
|
||||||
- a) Accompany the combined library with a copy of the same work
|
|
||||||
based on the Library, uncombined with any other library
|
|
||||||
facilities, conveyed under the terms of this License.
|
|
||||||
- b) Give prominent notice with the combined library that part of it
|
|
||||||
is a work based on the Library, and explaining where to find the
|
|
||||||
accompanying uncombined form of the same work.
|
|
||||||
|
|
||||||
#### 6. Revised Versions of the GNU Lesser General Public License.
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions
|
|
||||||
of the GNU Lesser General Public License from time to time. Such new
|
|
||||||
versions will be similar in spirit to the present version, but may
|
|
||||||
differ in detail to address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the Library
|
|
||||||
as you received it specifies that a certain numbered version of the
|
|
||||||
GNU Lesser General Public License "or any later version" applies to
|
|
||||||
it, you have the option of following the terms and conditions either
|
|
||||||
of that published version or of any later version published by the
|
|
||||||
Free Software Foundation. If the Library as you received it does not
|
|
||||||
specify a version number of the GNU Lesser General Public License, you
|
|
||||||
may choose any version of the GNU Lesser General Public License ever
|
|
||||||
published by the Free Software Foundation.
|
|
||||||
|
|
||||||
If the Library as you received it specifies that a proxy can decide
|
|
||||||
whether future versions of the GNU Lesser General Public License shall
|
|
||||||
apply, that proxy's public statement of acceptance of any version is
|
|
||||||
permanent authorization for you to choose that version for the
|
|
||||||
Library.
|
|
|
@ -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,6 +54,7 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Alerts and form errors */
|
/* Alerts and form errors */
|
||||||
.alert {
|
.alert {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
|
@ -87,47 +88,3 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
import type {Config} from '@jest/types';
|
|
||||||
// Sync object
|
|
||||||
const config: Config.InitialOptions = {
|
|
||||||
verbose: true,
|
|
||||||
transform: {
|
|
||||||
‘^.+\\.tsx?$’: ‘ts-jest’,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
export default config;
|
|
|
@ -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 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}})
|
||||||
|
@ -44,5 +43,5 @@ liveSocket.connect()
|
||||||
window.liveSocket = liveSocket
|
window.liveSocket = liveSocket
|
||||||
|
|
||||||
window.Components = {
|
window.Components = {
|
||||||
SplashPage, JustPage, ForPage, YouPage, AuthPage, PrivacyPolicyPage
|
SplashPage, JustPage, ForPage, YouPage, AuthPage
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,4 @@
|
||||||
type IntendedUser = {
|
type IntendedUser = {
|
||||||
name: string;
|
name: string;
|
||||||
emails: OAuthEmail[];
|
emails: string[];
|
||||||
username: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type OAuthEmail = {
|
|
||||||
email: string;
|
|
||||||
primary: boolean;
|
|
||||||
verified: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type IntendedLink = {
|
|
||||||
filename: string | null,
|
|
||||||
filetype: string | null,
|
|
||||||
text_content: string | null,
|
|
||||||
file_content: string | null
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,7 +20,6 @@ type AuthPageProps = {
|
||||||
service: string;
|
service: string;
|
||||||
recipient: string;
|
recipient: string;
|
||||||
user: IntendedUser | null;
|
user: IntendedUser | null;
|
||||||
error: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Keys {
|
interface Keys {
|
||||||
|
@ -28,107 +27,47 @@ interface Keys {
|
||||||
iv: string;
|
iv: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LinkFiles {
|
interface Link {
|
||||||
text: Blob | null;
|
text: Blob | null;
|
||||||
file: Blob | null;
|
file: Blob | null;
|
||||||
filename: string | null;
|
|
||||||
filetype: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GithubEmail {
|
|
||||||
email: string;
|
|
||||||
verified: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthPage = (props: AuthPageProps) => {
|
const AuthPage = (props: AuthPageProps) => {
|
||||||
const { service, recipient, user } = props;
|
const { service, recipient, user } = props;
|
||||||
|
|
||||||
const [secretFileUrl, setSecretFileUrl] = useState<string>("#");
|
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(() => {
|
useEffect(() => {
|
||||||
init().catch((reason) => {
|
init().catch((reason) => {
|
||||||
console.log(reason);
|
alert(reason);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const capitalize = (s: string) =>
|
|
||||||
(s && s[0].toUpperCase() + s.slice(1)) || "";
|
|
||||||
|
|
||||||
const init = async (): Promise<void> => {
|
const init = async (): Promise<void> => {
|
||||||
const link: LinkFiles | null = await retrieveLink();
|
|
||||||
const keys: Keys | null = await retrieveKeys();
|
const keys: Keys | null = await retrieveKeys();
|
||||||
if (link && keys && user) {
|
const link: Link | null = await retrieveLink();
|
||||||
|
if (link && keys) {
|
||||||
await decrypt(link, keys);
|
await decrypt(link, keys);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const userEmails = (): string[] => {
|
const retrieveLink = async (): Promise<Link | null> => {
|
||||||
if (!user?.emails) return [];
|
|
||||||
if (user.emails.length <= 0) return [];
|
|
||||||
return user
|
|
||||||
? user.emails
|
|
||||||
.filter(verifiedUserEmails)
|
|
||||||
.map((email) => (typeof email == "string" ? email : email.email))
|
|
||||||
: [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const isGithubEmail = (email: string | GithubEmail): email is GithubEmail =>
|
|
||||||
(email as GithubEmail).verified !== undefined;
|
|
||||||
|
|
||||||
const verifiedUserEmails = (email: string | GithubEmail) => {
|
|
||||||
if (isGithubEmail(email)) {
|
|
||||||
return (email as GithubEmail).verified;
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const retrieveLink = async (): Promise<LinkFiles | null> => {
|
|
||||||
const urlSegments = new URL(document.URL).pathname.split("/");
|
const urlSegments = new URL(document.URL).pathname.split("/");
|
||||||
const linkId = urlSegments.pop() || urlSegments.pop();
|
const linkId = urlSegments.pop() || urlSegments.pop();
|
||||||
if (!linkId) {
|
if (!linkId) {
|
||||||
alert("Could not find intended link in URL");
|
alert("Could not find intended link in URL");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!user) {
|
|
||||||
// no need to retrieve link if they weren't authenticated
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const linkResponse = await fetch(`/links/${linkId}`);
|
const textResponse = await fetch(`/uploads/links/${linkId}/text`);
|
||||||
let linkData: IntendedLink | null;
|
const textData = await textResponse.blob();
|
||||||
let textData = null;
|
const fileResponse = await fetch(`/uploads/links/${linkId}/file`);
|
||||||
let fileData = null;
|
const fileData = await fileResponse.blob();
|
||||||
if (linkResponse.status !== 200) {
|
|
||||||
throw new Error(linkResponse.statusText);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
linkData = await linkResponse.json();
|
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
text: textData,
|
text: textData.size > 0 ? textData : null,
|
||||||
file: fileData,
|
file: fileData.size > 0 ? fileData : null,
|
||||||
filename: linkData ? linkData.filename : null,
|
|
||||||
filetype: linkData ? linkData.filetype : null,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -140,13 +79,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("key_hex");
|
key = sessionStorage.getItem("link_key");
|
||||||
iv = sessionStorage.getItem("iv_hex");
|
iv = sessionStorage.getItem("link_iv");
|
||||||
} else {
|
} else {
|
||||||
key = fragmentData[0];
|
key = fragmentData[0];
|
||||||
iv = fragmentData[1];
|
iv = fragmentData[1];
|
||||||
sessionStorage.setItem("key_hex", key);
|
sessionStorage.setItem("link_key", key);
|
||||||
sessionStorage.setItem("iv_hex", iv);
|
sessionStorage.setItem("link_iv", iv);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key && iv) {
|
if (key && iv) {
|
||||||
|
@ -157,7 +96,7 @@ const AuthPage = (props: AuthPageProps) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const decrypt = async (link: LinkFiles, keys: Keys) => {
|
const decrypt = async (link: Link, keys: Keys) => {
|
||||||
const convertedKey = HexMix.hexToUint8(keys.key);
|
const convertedKey = HexMix.hexToUint8(keys.key);
|
||||||
const convertedIv = HexMix.hexToUint8(keys.iv);
|
const convertedIv = HexMix.hexToUint8(keys.iv);
|
||||||
const importedKey = await window.crypto.subtle.importKey(
|
const importedKey = await window.crypto.subtle.importKey(
|
||||||
|
@ -167,7 +106,6 @@ 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(
|
||||||
|
@ -182,79 +120,24 @@ const AuthPage = (props: AuthPageProps) => {
|
||||||
// And voila
|
// And voila
|
||||||
HexMix.arrayBufferToString(encodedText, (result: string) => {
|
HexMix.arrayBufferToString(encodedText, (result: string) => {
|
||||||
setSecretMessage(result);
|
setSecretMessage(result);
|
||||||
setMessageRevealed(true);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (link?.file) {
|
if (link?.file) {
|
||||||
const uploadedFile = await link.file.arrayBuffer();
|
|
||||||
const encodedFile = await window.crypto.subtle.decrypt(
|
|
||||||
{
|
|
||||||
name: "AES-GCM",
|
|
||||||
length: 256,
|
|
||||||
iv: convertedIv,
|
|
||||||
},
|
|
||||||
importedKey,
|
|
||||||
uploadedFile
|
|
||||||
);
|
|
||||||
|
|
||||||
const blob = new Blob([encodedFile], {
|
|
||||||
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 ? ` ${user.name}` : ""}! You are logged in to{" "}
|
|
||||||
<span style={{ color: "#A849CF" }}>{capitalize(service)}</span>
|
|
||||||
{user.username ? " as " : ""}
|
|
||||||
<span style={{ color: "#32EFE7" }}>
|
|
||||||
{user.username ? `${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 => {
|
const renderHeader = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Header2 style={{ marginBottom: ".4rem" }}>
|
<Header2 style={{ margin: ".4rem" }}>Someone sent you a secret</Header2>
|
||||||
{user ? "You have been identified!" : "Someone sent you a secret"}
|
|
||||||
</Header2>
|
|
||||||
{user ? (
|
{user ? (
|
||||||
<Header3 small>
|
<Header3 small>
|
||||||
{messageRevealed
|
Hello {user.name}, you are logged in {service} which has the
|
||||||
? "The following message and/or file is for your eyes only."
|
following verified emails: {user.emails.join(", ")}
|
||||||
: "Unfortunately, you are not the intended recipient."}
|
|
||||||
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
</Header3>
|
</Header3>
|
||||||
) : (
|
) : (
|
||||||
<Header3 small>
|
<Header3 small>
|
||||||
The intended recipient for this message is {recipient} on{" "}
|
Please verify your identity to reveal this message.
|
||||||
{capitalize(service)}. Please verify your identity to reveal this
|
|
||||||
message.
|
|
||||||
</Header3>
|
</Header3>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -263,7 +146,7 @@ const AuthPage = (props: AuthPageProps) => {
|
||||||
|
|
||||||
const renderAuth = (): JSX.Element => {
|
const renderAuth = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<CenteredContainer fullscreen className="centered-container">
|
<CenteredContainer fullscreen>
|
||||||
<CenteredContainer>
|
<CenteredContainer>
|
||||||
{renderHeader()}
|
{renderHeader()}
|
||||||
<Spacer space="3rem" />
|
<Spacer space="3rem" />
|
||||||
|
@ -293,7 +176,7 @@ const AuthPage = (props: AuthPageProps) => {
|
||||||
|
|
||||||
const renderReveal = (): JSX.Element => {
|
const renderReveal = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<CenteredContainer fullscreen className="centered-container">
|
<CenteredContainer fullscreen>
|
||||||
<CenteredContainer>
|
<CenteredContainer>
|
||||||
{renderHeader()}
|
{renderHeader()}
|
||||||
<Spacer space="3rem" />
|
<Spacer space="3rem" />
|
||||||
|
@ -310,22 +193,18 @@ const AuthPage = (props: AuthPageProps) => {
|
||||||
<TextAlignWrapper align="left">
|
<TextAlignWrapper align="left">
|
||||||
<Label htmlFor="service">Secret File</Label>
|
<Label htmlFor="service">Secret File</Label>
|
||||||
</TextAlignWrapper>
|
</TextAlignWrapper>
|
||||||
<a href={secretFileUrl} download style={{ width: "100%" }}>
|
<InputButtonWithIcon
|
||||||
<InputButtonWithIcon
|
variant="download"
|
||||||
variant="download"
|
id="downloadfile"
|
||||||
id="downloadfile"
|
value={secretFileUrl}
|
||||||
value={secretFileName}
|
onClick={() => {}}
|
||||||
onClick={() => {}}
|
/>
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<Spacer space="3rem" />
|
<Spacer space="3rem" />
|
||||||
<a href={`https://intended.link/auth/logout`}>
|
<a href={`https://intended.link/auth/${service}`}>
|
||||||
<Button variant="primary" wide onClick={() => {}}>
|
<Button variant="primary" wide onClick={() => {}}>
|
||||||
Logout
|
Re-Verify
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
<Spacer space="3rem" />
|
|
||||||
{renderFooter()}
|
|
||||||
</CenteredContainer>
|
</CenteredContainer>
|
||||||
</CenteredContainer>
|
</CenteredContainer>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,23 +1,11 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import {
|
import { ProgressIndicator, Header2, Button, IconArrow, Label, Input, Select, CenteredContainer, SpaceBetweenContainer, Spacer, TextAlignWrapper, GlobalStyle } from "@intended/intended-ui";
|
||||||
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("");
|
||||||
|
@ -34,6 +22,12 @@ 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");
|
||||||
|
@ -41,20 +35,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();
|
||||||
|
@ -67,7 +61,7 @@ const ForPage = (props: ForPageProps) => {
|
||||||
return (
|
return (
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<GlobalStyle />
|
<GlobalStyle />
|
||||||
<CenteredContainer fullscreen className="centered-container">
|
<CenteredContainer fullscreen>
|
||||||
<CenteredContainer>
|
<CenteredContainer>
|
||||||
<ProgressIndicator currentProgress={2} />
|
<ProgressIndicator currentProgress={2} />
|
||||||
<Header2>Tell Someone</Header2>
|
<Header2>Tell Someone</Header2>
|
||||||
|
@ -85,8 +79,7 @@ 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
|
What type of account is the above username or email associated with?
|
||||||
with?
|
|
||||||
</Label>
|
</Label>
|
||||||
</TextAlignWrapper>
|
</TextAlignWrapper>
|
||||||
<Select
|
<Select
|
||||||
|
@ -94,15 +87,11 @@ const ForPage = (props: ForPageProps) => {
|
||||||
onChange={handleServiceChange}
|
onChange={handleServiceChange}
|
||||||
value={serviceSelect}
|
value={serviceSelect}
|
||||||
>
|
>
|
||||||
<option value="github">Github</option>
|
<option value='github'>Github</option>
|
||||||
<option value="google">Gmail</option>
|
</Select>
|
||||||
</Select>
|
|
||||||
<Spacer space="3rem" />
|
<Spacer space="3rem" />
|
||||||
<SpaceBetweenContainer>
|
<SpaceBetweenContainer>
|
||||||
<Button
|
<Button variant="secondary" onClick={() => window.location.href = "/just"}>
|
||||||
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>
|
||||||
|
|
|
@ -1,173 +1,80 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
import {
|
import { ProgressIndicator, Header2, Button, IconArrow, Label, FileInput, TextArea, CenteredContainer, Spacer, TextAlignWrapper, GlobalStyle } from '@intended/intended-ui';
|
||||||
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";
|
||||||
|
|
||||||
type JustPageProps = {
|
type JustPageProps = {
|
||||||
csrf: string;
|
csrf: string
|
||||||
};
|
|
||||||
|
|
||||||
interface AESKey {
|
|
||||||
key: CryptoKey;
|
|
||||||
iv: Uint8Array;
|
|
||||||
exported: ArrayBuffer;
|
|
||||||
keyHex: string;
|
|
||||||
ivHex: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const JustPage = (props: JustPageProps) => {
|
const JustPage = (props: JustPageProps) => {
|
||||||
const [secretInput, setSecretInput] = useState("");
|
const [secretInput, setSecretInput] = useState("");
|
||||||
const [fileInput, setFileInput] = useState<string | null>(null);
|
const [fileInput, setFileInput] = useState<File | null>(null);
|
||||||
const [fileName, setFileName] = useState("");
|
const [fileName, setFileName] = useState("");
|
||||||
const [fileType, setFileType] = useState("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sessionStorage.clear();
|
sessionStorage.clear();
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
setSecretInput(e.target.value);
|
setSecretInput(e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFile = (file: File) => {
|
const handleFile = (file: File) => {
|
||||||
|
setFileInput(file);
|
||||||
|
|
||||||
setFileName(file.name);
|
setFileName(file.name);
|
||||||
setFileType(file.type);
|
|
||||||
|
|
||||||
if (file instanceof File) {
|
|
||||||
if (file.size > 2097152) {
|
|
||||||
// TODO: may not need this check
|
|
||||||
alert("Error: Max file size is 2mb.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onloadend = loadFile;
|
|
||||||
reader.readAsArrayBuffer(file);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadFile = (fileEvent: ProgressEvent<FileReader>) => {
|
|
||||||
// Make sure we actually got a binary file
|
|
||||||
if (
|
|
||||||
fileEvent &&
|
|
||||||
fileEvent.target &&
|
|
||||||
fileEvent.target.result instanceof ArrayBuffer
|
|
||||||
) {
|
|
||||||
const data = fileEvent.target.result as ArrayBuffer;
|
|
||||||
HexMix.arrayBufferToString(data, (result: string) => {
|
|
||||||
setFileInput(result);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
alert("File is either missing or corrupt.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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(
|
|
||||||
{
|
|
||||||
name: "AES-GCM",
|
|
||||||
iv: aesKey.iv,
|
|
||||||
},
|
|
||||||
aesKey.key,
|
|
||||||
encoded
|
|
||||||
);
|
|
||||||
const blobData = new Blob([encrypted]);
|
|
||||||
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
|
|
||||||
): Promise<FormData> => {
|
|
||||||
if (!secretInput) return form;
|
|
||||||
const encoded = HexMix.stringToArrayBuffer(secretInput);
|
|
||||||
const encrypted = await window.crypto.subtle.encrypt(
|
|
||||||
{
|
|
||||||
name: "AES-GCM",
|
|
||||||
iv: aesKey.iv,
|
|
||||||
},
|
|
||||||
aesKey.key,
|
|
||||||
encoded
|
|
||||||
);
|
|
||||||
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> => {
|
|
||||||
const key = await window.crypto.subtle.generateKey(
|
|
||||||
{
|
|
||||||
name: "AES-GCM",
|
|
||||||
length: 256,
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
["encrypt", "decrypt"]
|
|
||||||
);
|
|
||||||
|
|
||||||
const iv = window.crypto.getRandomValues(new Uint8Array(16));
|
|
||||||
const exported = await window.crypto.subtle.exportKey("raw", key);
|
|
||||||
const keyHex = HexMix.uint8ToHex(new Uint8Array(exported));
|
|
||||||
const ivHex = HexMix.uint8ToHex(iv);
|
|
||||||
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
iv,
|
|
||||||
exported,
|
|
||||||
keyHex,
|
|
||||||
ivHex,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const postContents = async () => {
|
const postContents = async () => {
|
||||||
if (!window.crypto.subtle) {
|
if (!window.crypto.subtle) {
|
||||||
alert("Browser does not support SubtleCrypto");
|
alert('Browser does not support SubtleCrypto');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = await createKey();
|
const key = await window.crypto.subtle.generateKey(
|
||||||
let formData = new FormData();
|
{
|
||||||
formData = await fileFormData(formData, key);
|
name: 'AES-GCM',
|
||||||
formData = await textFormData(formData, key);
|
length: 256
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
['encrypt', 'decrypt']
|
||||||
|
);
|
||||||
|
const encoded = HexMix.stringToArrayBuffer(secretInput);
|
||||||
|
const iv = window.crypto.getRandomValues(new Uint8Array(16));
|
||||||
|
const exported = await window.crypto.subtle.exportKey('raw', key);
|
||||||
|
const encrypted = await window.crypto.subtle.encrypt(
|
||||||
|
{
|
||||||
|
name: 'AES-GCM',
|
||||||
|
iv
|
||||||
|
},
|
||||||
|
key,
|
||||||
|
encoded
|
||||||
|
);
|
||||||
|
|
||||||
|
const keyHex = HexMix.uint8ToHex(new Uint8Array(exported));
|
||||||
|
const ivHex = HexMix.uint8ToHex(iv);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
const blobData = new Blob([encrypted]);
|
||||||
|
|
||||||
|
formData.append('text_content', blobData);
|
||||||
|
// formData.append('filetype', 'text/plain');
|
||||||
|
// formData.append('filename', 'secret.txt');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const link: Response = 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"
|
||||||
});
|
});
|
||||||
const { id: link_id } = await link.json();
|
const { id: link_id } = await link.json()
|
||||||
|
|
||||||
sessionStorage.setItem("link_id", link_id);
|
sessionStorage.setItem("link_id", link_id);
|
||||||
sessionStorage.setItem("key_hex", key.keyHex);
|
sessionStorage.setItem("key_hex", keyHex);
|
||||||
sessionStorage.setItem("iv_hex", key.ivHex);
|
sessionStorage.setItem("iv_hex", ivHex);
|
||||||
window.location.href = `${window.location.origin}/just/for`;
|
window.location.href = `${window.location.origin}/just/for`;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
alert(err.message);
|
alert(err.message);
|
||||||
|
@ -177,18 +84,18 @@ const JustPage = (props: JustPageProps) => {
|
||||||
return (
|
return (
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<GlobalStyle />
|
<GlobalStyle />
|
||||||
<CenteredContainer fullscreen className="centered-container">
|
<CenteredContainer fullscreen>
|
||||||
<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 a secret message</Label>
|
<Label htmlFor="secretInput">Enter your secret here</Label>
|
||||||
</TextAlignWrapper>
|
</TextAlignWrapper>
|
||||||
<TextArea
|
<TextArea
|
||||||
id="secretInput"
|
id="secretInput"
|
||||||
value={secretInput}
|
value={secretInput}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder="Only your intended recipient will see this message."
|
placeholder="Tell me your secrets"
|
||||||
/>
|
/>
|
||||||
<Spacer space="2rem" />
|
<Spacer space="2rem" />
|
||||||
<TextAlignWrapper align="center">
|
<TextAlignWrapper align="center">
|
||||||
|
@ -196,13 +103,10 @@ const JustPage = (props: JustPageProps) => {
|
||||||
</TextAlignWrapper>
|
</TextAlignWrapper>
|
||||||
<Spacer space="1.6rem" />
|
<Spacer space="1.6rem" />
|
||||||
<FileInput id="fileInput" value={fileName} handleFile={handleFile} />
|
<FileInput id="fileInput" value={fileName} handleFile={handleFile} />
|
||||||
|
{ fileInput ? "" : ""}
|
||||||
<Spacer space="4rem" />
|
<Spacer space="4rem" />
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{ display: "flex", justifyContent: "flex-end", width: "100%" }}
|
||||||
display: "flex",
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Button variant="secondary" onClick={postContents}>
|
<Button variant="secondary" onClick={postContents}>
|
||||||
<IconArrow arrowDirection="right" />
|
<IconArrow arrowDirection="right" />
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
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;
|
|
|
@ -1,65 +1,21 @@
|
||||||
import React, { useEffect } from "react";
|
import React from "react";
|
||||||
|
|
||||||
import {
|
import { CenteredContainer, SplashIconHeader, Header1, Header3, Spacer, Button, GlobalStyle } from '@intended/intended-ui';
|
||||||
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 (
|
return (
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<GlobalStyle />
|
<GlobalStyle />
|
||||||
<CenteredContainer
|
<CenteredContainer fullscreen>
|
||||||
fullscreen
|
|
||||||
style={{
|
|
||||||
background: "none",
|
|
||||||
position: "absolute",
|
|
||||||
top: "50%",
|
|
||||||
transform: "translate(0, -50%)",
|
|
||||||
height: "auto",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CenteredContainer wide>
|
<CenteredContainer wide>
|
||||||
<SplashIconHeader style={{ width: "100%", maxWidth: "440px" }} />
|
<SplashIconHeader />
|
||||||
<Header1>
|
<Header1>Securely Share Your Secrets</Header1>
|
||||||
<span
|
|
||||||
className="splashHeader"
|
|
||||||
style={{ display: "block", marginTop: "20px" }}
|
|
||||||
>
|
|
||||||
Securely Share Your Secrets
|
|
||||||
</span>
|
|
||||||
</Header1>
|
|
||||||
<Header3>
|
<Header3>
|
||||||
<span className="splashSubheader">
|
With Intended Link you can easily share messages and files securely
|
||||||
With Intended Link, you can send messages and files to any social
|
and secretly.
|
||||||
account in a secure and private manner.
|
|
||||||
</span>
|
|
||||||
</Header3>
|
</Header3>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<Button
|
<Button variant="secondary" boldFont onClick={() => window.location.href = "/just"}>
|
||||||
variant="secondary"
|
|
||||||
boldFont
|
|
||||||
onClick={() => (window.location.href = "/just")}
|
|
||||||
>
|
|
||||||
START SHARING
|
START SHARING
|
||||||
</Button>
|
</Button>
|
||||||
</CenteredContainer>
|
</CenteredContainer>
|
||||||
|
|
|
@ -1,29 +1,8 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import {
|
import { ProgressIndicator, Header2, Button, IconArrow, InputButtonWithIcon, Label, Input, CenteredContainer, SpaceBetweenContainer, Spacer, TextAlignWrapper, GlobalStyle } from "@intended/intended-ui";
|
||||||
ProgressIndicator,
|
|
||||||
Header2,
|
|
||||||
Button,
|
|
||||||
IconArrow,
|
|
||||||
InputButtonWithIcon,
|
|
||||||
Label,
|
|
||||||
Input,
|
|
||||||
CenteredContainer,
|
|
||||||
SpaceBetweenContainer,
|
|
||||||
Spacer,
|
|
||||||
TextAlignWrapper,
|
|
||||||
GlobalStyle,
|
|
||||||
} from "@intended/intended-ui";
|
|
||||||
|
|
||||||
const YouPage = () => {
|
const YouPage = () => {
|
||||||
const [url, setUrl] = useState("#");
|
|
||||||
const [encoded, setEncoded] = useState("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setUrl(calculateUrl());
|
|
||||||
setEncoded(calculateEncoded());
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const calculateUrl = () => {
|
const calculateUrl = () => {
|
||||||
const linkId = sessionStorage.getItem("link_id");
|
const linkId = sessionStorage.getItem("link_id");
|
||||||
const keyHex = sessionStorage.getItem("key_hex");
|
const keyHex = sessionStorage.getItem("key_hex");
|
||||||
|
@ -32,24 +11,20 @@ const YouPage = () => {
|
||||||
return `${window.location.origin}/just/for/you/${linkId}#${keyHex}.${ivHex}`;
|
return `${window.location.origin}/just/for/you/${linkId}#${keyHex}.${ivHex}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateEncoded = () => {
|
const [url, _setUrl] = useState(calculateUrl());
|
||||||
const encodedFile = sessionStorage.getItem("encoded_file");
|
|
||||||
const encodedMessage = sessionStorage.getItem("encoded_message");
|
|
||||||
return `${encodedMessage}${encodedFile}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyUrl = async () => {
|
const copyUrl = async () => {
|
||||||
try {
|
try {
|
||||||
navigator.clipboard.writeText(url);
|
navigator.clipboard.writeText(url);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
alert("Could not copy url to clipboard.");
|
alert("Could not copy url to clipboard.");
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<GlobalStyle />
|
<GlobalStyle />
|
||||||
<CenteredContainer fullscreen className="centered-container">
|
<CenteredContainer fullscreen>
|
||||||
<CenteredContainer>
|
<CenteredContainer>
|
||||||
<ProgressIndicator currentProgress={3} />
|
<ProgressIndicator currentProgress={3} />
|
||||||
<Header2>Share the secret</Header2>
|
<Header2>Share the secret</Header2>
|
||||||
|
@ -72,23 +47,20 @@ const YouPage = () => {
|
||||||
looking eh?:
|
looking eh?:
|
||||||
</Label>
|
</Label>
|
||||||
</TextAlignWrapper>
|
</TextAlignWrapper>
|
||||||
<Input variant="disabled-light" id="encodedSecret" value={encoded} />
|
<Input
|
||||||
|
variant="disabled-light"
|
||||||
|
id="encodedSecret"
|
||||||
|
value={url}
|
||||||
|
/>
|
||||||
<Spacer space="3rem" />
|
<Spacer space="3rem" />
|
||||||
<SpaceBetweenContainer>
|
<SpaceBetweenContainer>
|
||||||
<Button
|
<Button variant="secondary" onClick={() => window.location.href = "/just/for"}>
|
||||||
variant="secondary"
|
|
||||||
onClick={() => (window.location.href = "/just/for")}
|
|
||||||
>
|
|
||||||
<IconArrow arrowDirection="left" />
|
<IconArrow arrowDirection="left" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button onClick={() => {
|
||||||
onClick={() => {
|
sessionStorage.clear();
|
||||||
sessionStorage.clear();
|
window.location.href = "/just";
|
||||||
window.location.href = "/just";
|
}}>Create Another Secret</Button>
|
||||||
}}
|
|
||||||
>
|
|
||||||
Create Another Secret
|
|
||||||
</Button>
|
|
||||||
</SpaceBetweenContainer>
|
</SpaceBetweenContainer>
|
||||||
</CenteredContainer>
|
</CenteredContainer>
|
||||||
</CenteredContainer>
|
</CenteredContainer>
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -4,11 +4,10 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"deploy": "webpack --mode production",
|
"deploy": "webpack --mode production",
|
||||||
"watch": "webpack --mode development --watch",
|
"watch": "webpack --mode development --watch"
|
||||||
"test": "jest"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intended/intended-ui": "0.1.26",
|
"@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",
|
||||||
|
@ -21,7 +20,6 @@
|
||||||
"@babel/core": "^7.0.0",
|
"@babel/core": "^7.0.0",
|
||||||
"@babel/preset-env": "^7.0.0",
|
"@babel/preset-env": "^7.0.0",
|
||||||
"@babel/preset-react": "^7.16.0",
|
"@babel/preset-react": "^7.16.0",
|
||||||
"@types/jest": "^27.4.1",
|
|
||||||
"@types/phoenix": "^1.5.3",
|
"@types/phoenix": "^1.5.3",
|
||||||
"@types/react": "^17.0.37",
|
"@types/react": "^17.0.37",
|
||||||
"@types/react-dom": "^17.0.11",
|
"@types/react-dom": "^17.0.11",
|
||||||
|
@ -29,14 +27,12 @@
|
||||||
"copy-webpack-plugin": "^5.1.1",
|
"copy-webpack-plugin": "^5.1.1",
|
||||||
"css-loader": "^3.4.2",
|
"css-loader": "^3.4.2",
|
||||||
"hard-source-webpack-plugin": "^0.13.1",
|
"hard-source-webpack-plugin": "^0.13.1",
|
||||||
"jest": "^27.5.1",
|
|
||||||
"mini-css-extract-plugin": "^0.9.0",
|
"mini-css-extract-plugin": "^0.9.0",
|
||||||
"node-sass": "^4.13.1",
|
"node-sass": "^4.13.1",
|
||||||
"optimize-css-assets-webpack-plugin": "^5.0.1",
|
"optimize-css-assets-webpack-plugin": "^5.0.1",
|
||||||
"sass-loader": "^8.0.2",
|
"sass-loader": "^8.0.2",
|
||||||
"source-map-loader": "^3.0.0",
|
"source-map-loader": "^3.0.0",
|
||||||
"terser-webpack-plugin": "^2.3.2",
|
"terser-webpack-plugin": "^2.3.2",
|
||||||
"ts-jest": "^27.1.3",
|
|
||||||
"ts-loader": "8.2.0",
|
"ts-loader": "8.2.0",
|
||||||
"typescript": "^4.5.2",
|
"typescript": "^4.5.2",
|
||||||
"webpack": "^4.41.5",
|
"webpack": "^4.41.5",
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 4.4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 156 KiB |
|
@ -1,3 +0,0 @@
|
||||||
## Implementation Details
|
|
||||||
|
|
||||||
Write info on what we'd use, or just start adding in the tests and remove this file.
|
|
|
@ -30,9 +30,7 @@ config :phoenix, :json_library, Jason
|
||||||
|
|
||||||
config :ueberauth, Ueberauth,
|
config :ueberauth, Ueberauth,
|
||||||
providers: [
|
providers: [
|
||||||
github:
|
github: {Ueberauth.Strategy.Github, [default_scope: "user:email", allow_private_emails: true]}
|
||||||
{Ueberauth.Strategy.Github, [default_scope: "user:email", allow_private_emails: true]},
|
|
||||||
google: {Ueberauth.Strategy.Google, [default_scope: "email"]}
|
|
||||||
]
|
]
|
||||||
|
|
||||||
config :waffle,
|
config :waffle,
|
||||||
|
|
|
@ -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: "intended.link", port: 80],
|
url: [host: "example.com", 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,7 +45,8 @@ 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 :my_app, EntenduWeb.Endpoint, force_ssl: [rewrite_on: [:x_forwarded_proto]]
|
# config :entendu, EntenduWeb.Endpoint,
|
||||||
|
# force_ssl: [hsts: true]
|
||||||
#
|
#
|
||||||
# Check `Plug.SSL` for all available options in `force_ssl`.
|
# Check `Plug.SSL` for all available options in `force_ssl`.
|
||||||
|
|
||||||
|
|
|
@ -34,10 +34,6 @@ config :ueberauth, Ueberauth.Strategy.Github.OAuth,
|
||||||
client_id: System.get_env("GH_OAUTH_ID"),
|
client_id: System.get_env("GH_OAUTH_ID"),
|
||||||
client_secret: System.get_env("GH_OAUTH_SECRET")
|
client_secret: System.get_env("GH_OAUTH_SECRET")
|
||||||
|
|
||||||
config :ueberauth, Ueberauth.Strategy.Google.OAuth,
|
|
||||||
client_id: System.get_env("GOOGLE_OAUTH_ID"),
|
|
||||||
client_secret: System.get_env("GOOGLE_OAUTH_SECRET")
|
|
||||||
|
|
||||||
# ## Using releases (Elixir v1.9+)
|
# ## Using releases (Elixir v1.9+)
|
||||||
#
|
#
|
||||||
# If you are doing OTP releases, you need to instruct Phoenix
|
# If you are doing OTP releases, you need to instruct Phoenix
|
||||||
|
|
|
@ -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)}
|
||||||
|
@ -70,22 +71,7 @@ defmodule Entendu.UserFromAuth do
|
||||||
do: email_matches?(recipient, emails) || username_matches?(recipient, username)
|
do: email_matches?(recipient, emails) || username_matches?(recipient, username)
|
||||||
|
|
||||||
defp email_matches?(recipient, emails),
|
defp email_matches?(recipient, emails),
|
||||||
do:
|
do: emails |> Enum.any?(&(&1["verified"] == true and &1["email"] == recipient))
|
||||||
emails
|
|
||||||
|> Enum.filter(&only_verified_emails/1)
|
|
||||||
|> Enum.map(&retrieve_email/1)
|
|
||||||
|> Enum.any?(&(&1 == recipient))
|
|
||||||
|
|
||||||
# Github lists unverified emails and need to be filtered out
|
|
||||||
defp only_verified_emails(%{"verified" => is_verified}), do: is_verified
|
|
||||||
|
|
||||||
defp only_verified_emails(_), do: true
|
|
||||||
|
|
||||||
defp retrieve_email(%{"email" => email}), do: email
|
|
||||||
|
|
||||||
defp retrieve_email(email), do: email
|
|
||||||
|
|
||||||
defp username_matches?(_recipient, nil), do: false
|
|
||||||
|
|
||||||
defp username_matches?(recipient, username), do: String.trim(username) === recipient
|
defp username_matches?(recipient, username), do: String.trim(username) === recipient
|
||||||
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
|
||||||
|
@ -25,8 +27,11 @@ defmodule EntenduWeb.AuthController do
|
||||||
def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
|
def callback(%{assigns: %{ueberauth_auth: auth}} = conn, _params) do
|
||||||
link = get_session(conn, :intended_link)
|
link = get_session(conn, :intended_link)
|
||||||
|
|
||||||
with %{id: link_id} <- link,
|
with %{id: link_id, recipient: recipient} <- link,
|
||||||
{:ok, user} <- UserFromAuth.find_or_create(auth) do
|
{:ok, user} <- UserFromAuth.find_or_create(auth),
|
||||||
|
true <- UserFromAuth.can_access?(recipient, user) do
|
||||||
|
# TODO: send over encrypted data that the frontend can decrypt
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_session(:current_user, user)
|
|> put_session(:current_user, user)
|
||||||
|> configure_session(renew: true)
|
|> configure_session(renew: true)
|
||||||
|
@ -37,6 +42,11 @@ 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)
|
||||||
|
|
|
@ -9,6 +9,8 @@ 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
|
alias EntenduWeb.Plugs.AuthorizeLink
|
||||||
|
|
||||||
plug AuthorizeLink when action in [:text, :file]
|
plug AuthorizeLink when action in [:text, :file]
|
||||||
|
@ -16,8 +18,7 @@ defmodule EntenduWeb.LinkController do
|
||||||
action_fallback(FallbackController)
|
action_fallback(FallbackController)
|
||||||
|
|
||||||
def just_page(conn, _params) do
|
def just_page(conn, _params) do
|
||||||
conn
|
render(conn, "just.html")
|
||||||
|> render("just.html")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def just(conn, params) do
|
def just(conn, params) do
|
||||||
|
@ -35,7 +36,6 @@ 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,17 +45,28 @@ defmodule EntenduWeb.LinkController do
|
||||||
end
|
end
|
||||||
|
|
||||||
def auth_page(conn, %{"id" => link_id}) do
|
def auth_page(conn, %{"id" => link_id}) do
|
||||||
with %Link{id: id, service: service, recipient: recipient} <- Links.get_link(link_id) do
|
with %Link{service: service, recipient: recipient} = link <- Links.get_link(link_id) do
|
||||||
conn
|
conn
|
||||||
|> put_session(:intended_link, %{id: id, service: service, recipient: recipient})
|
|> put_session(:intended_link, link)
|
||||||
|> render("auth.html", %{intended_link: %{service: service, recipient: recipient}})
|
|> render("auth.html", %{intended_link: %{service: service, recipient: recipient}})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def authorized_link(conn, %{"id" => link_id}) do
|
def text(conn, %{"id" => link_id}) do
|
||||||
with %Link{} = link <- Links.get_link(link_id) do
|
with user = get_session(conn, :current_user),
|
||||||
conn
|
%Link{recipient: recipient} = link <- Links.get_link(link_id),
|
||||||
|> render("show_authorized.json", %{link: link})
|
true <- UserFromAuth.can_access?(recipient, user) 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) do
|
||||||
|
path = EncryptedLink.url({link.file_content, link})
|
||||||
|
send_file(conn, 200, path)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,13 +6,6 @@ defmodule EntenduWeb.PageController do
|
||||||
use EntenduWeb, :controller
|
use EntenduWeb, :controller
|
||||||
|
|
||||||
def index(conn, _params) do
|
def index(conn, _params) do
|
||||||
conn
|
render(conn, "index.html", current_user: get_session(conn, :current_user))
|
||||||
|> clear_session()
|
|
||||||
|> render("index.html")
|
|
||||||
end
|
|
||||||
|
|
||||||
def privacy(conn, _params) do
|
|
||||||
conn
|
|
||||||
|> render("privacy_policy.html")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,30 +2,29 @@ defmodule EntenduWeb.Plugs.AuthorizeLink do
|
||||||
import Plug.Conn
|
import Plug.Conn
|
||||||
use EntenduWeb, :controller
|
use EntenduWeb, :controller
|
||||||
|
|
||||||
|
alias Entendu.Repo
|
||||||
alias Entendu.UserFromAuth
|
alias Entendu.UserFromAuth
|
||||||
alias Entendu.Links
|
alias Entendu.Links
|
||||||
alias Entendu.Links.Link
|
alias Entendu.Links.Link
|
||||||
|
alias EntenduWeb.FallbackController
|
||||||
alias EntenduWeb.ErrorView
|
alias EntenduWeb.ErrorView
|
||||||
|
|
||||||
def init(_params) do
|
def init(_params) do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_link_id(%{params: %{"id" => link_id}}), do: link_id
|
def call(conn, params) do
|
||||||
|
%{params: %{"path" => [_, link_id, _]}} = conn
|
||||||
defp get_link_id(%{params: %{"path" => [_, link_id, _]}}), do: link_id
|
|
||||||
|
|
||||||
def call(conn, _params) do
|
|
||||||
link_id = get_link_id(conn)
|
|
||||||
user = get_session(conn, :current_user)
|
user = get_session(conn, :current_user)
|
||||||
|
|
||||||
if !user do
|
if !user do
|
||||||
conn
|
conn
|
||||||
|> put_status(403)
|
|> put_status(403)
|
||||||
|> put_view(ErrorView)
|
|> put_view(EntenduWeb.ErrorView)
|
||||||
|> render("error_code.json", message: "Unauthorized", code: 403)
|
|> render("error_code.json", message: "Unauthorized", code: 403)
|
||||||
|> halt
|
|> halt
|
||||||
else
|
else
|
||||||
with %Link{recipient: recipient} = link <- Links.get_link(link_id),
|
with {:ok, user} <- get_user_from_path(conn),
|
||||||
|
%Link{recipient: recipient} = link <- Links.get_link(link_id),
|
||||||
true <- UserFromAuth.can_access?(recipient, user) do
|
true <- UserFromAuth.can_access?(recipient, user) do
|
||||||
conn
|
conn
|
||||||
|> assign(:link, link)
|
|> assign(:link, link)
|
||||||
|
@ -33,24 +32,39 @@ defmodule EntenduWeb.Plugs.AuthorizeLink do
|
||||||
nil ->
|
nil ->
|
||||||
conn
|
conn
|
||||||
|> put_status(404)
|
|> put_status(404)
|
||||||
|> put_view(ErrorView)
|
|> put_view(EntenduWeb.ErrorView)
|
||||||
|> render("error_code.json", message: "Link could not be found", code: 404)
|
|> render("error_code.json", message: "Link could not be found", code: 404)
|
||||||
|> halt
|
|> halt
|
||||||
|
|
||||||
false ->
|
false ->
|
||||||
conn
|
conn
|
||||||
|> put_status(403)
|
|> put_status(403)
|
||||||
|> put_view(ErrorView)
|
|> put_view(EntenduWeb.ErrorView)
|
||||||
|> render("error_code.json", message: "Unauthorized", code: 403)
|
|> render("error_code.json", message: "Unauthorized", code: 403)
|
||||||
|> halt
|
|> halt
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
conn
|
conn
|
||||||
|> put_status(422)
|
|> put_status(422)
|
||||||
|> put_view(ErrorView)
|
|> put_view(EntenduWeb.ErrorView)
|
||||||
|> render("error_code.json", message: reason, code: 422)
|
|> render("error_code.json", message: reason, code: 422)
|
||||||
|> halt
|
|> halt
|
||||||
end
|
end
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -16,13 +16,9 @@ defmodule EntenduWeb.Router do
|
||||||
plug :accepts, ["json"]
|
plug :accepts, ["json"]
|
||||||
end
|
end
|
||||||
|
|
||||||
pipeline :authorized_files do
|
pipeline :authorized_links do
|
||||||
plug AuthorizeLink
|
|
||||||
plug Plug.Static, at: "/uploads", from: {:entendu, "priv/uploads"}, gzip: false
|
|
||||||
end
|
|
||||||
|
|
||||||
pipeline :authorized_link do
|
|
||||||
plug AuthorizeLink
|
plug AuthorizeLink
|
||||||
|
plug Plug.Static, at: "/uploads", from: Path.expand('./uploads'), gzip: false
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/", EntenduWeb do
|
scope "/", EntenduWeb do
|
||||||
|
@ -35,28 +31,23 @@ 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 "/privacy-policy", PageController, :privacy
|
get "/links/:id/file", LinkController, :file
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/auth", EntenduWeb do
|
scope "/auth", EntenduWeb do
|
||||||
pipe_through :browser
|
pipe_through :browser
|
||||||
|
|
||||||
get "/logout", AuthController, :delete
|
|
||||||
get "/:provider", AuthController, :request
|
get "/:provider", AuthController, :request
|
||||||
get "/:provider/callback", AuthController, :callback
|
get "/:provider/callback", AuthController, :callback
|
||||||
|
delete "/logout", AuthController, :delete
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/uploads", EntenduWeb do
|
scope "/uploads", EntenduWeb do
|
||||||
pipe_through [:browser, :authorized_files]
|
pipe_through [:browser, :authorized_links]
|
||||||
get "/*path", FileNotFoundController, :index
|
get "/*path", FileNotFoundController, :index
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/links", EntenduWeb do
|
|
||||||
pipe_through [:browser, :authorized_link]
|
|
||||||
get "/:id", LinkController, :authorized_link
|
|
||||||
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,6 +1,5 @@
|
||||||
<main role="main">
|
<main role="main">
|
||||||
<a href="/">
|
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
|
||||||
<img src="<%= Routes.static_path(@conn, "/images/logo.png") %>" class="logo" />
|
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
|
||||||
</a>
|
|
||||||
<%= @inner_content %>
|
<%= @inner_content %>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -4,24 +4,12 @@
|
||||||
<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] || "Intended Link", suffix: "" %>
|
<%= live_title_tag assigns[:page_title] || "Entendu", suffix: " · Phoenix Framework" %>
|
||||||
<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: linear-gradient(180deg,#060b2e 0%,#051745 100%); min-height: 100%;">
|
<body style="background: #060b2e;">
|
||||||
<%= @inner_content %>
|
<%= @inner_content %>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -3,6 +3,5 @@
|
||||||
service: @intended_link.service,
|
service: @intended_link.service,
|
||||||
recipient: @intended_link.recipient,
|
recipient: @intended_link.recipient,
|
||||||
user: current_user(@conn),
|
user: current_user(@conn),
|
||||||
link: current_link(@conn),
|
link: current_link(@conn)
|
||||||
error: get_flash(@conn, :error)
|
|
||||||
}) %>
|
}) %>
|
||||||
|
|
|
@ -1,5 +1,46 @@
|
||||||
<section>
|
<section>
|
||||||
|
|
||||||
<%= react_component("Components.SplashPage", %{ error: get_flash(@conn, :error) }) %>
|
<%= react_component("Components.SplashPage") %>
|
||||||
|
|
||||||
|
<%= 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>
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
<section>
|
|
||||||
<%= react_component("Components.PrivacyPolicyPage") %>
|
|
||||||
</section>
|
|
|
@ -31,13 +31,13 @@ defmodule Entendu.EncryptedLink do
|
||||||
# end
|
# end
|
||||||
|
|
||||||
# Override the persisted filenames:
|
# Override the persisted filenames:
|
||||||
# def filename(_version, {_file, %{filename: filename}}) do
|
def filename(_version, {_file, %{filename: filename}}) do
|
||||||
# if filename, do: filename, else: "text"
|
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
|
||||||
"priv/uploads/links/#{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
|
||||||
|
|
1
mix.exs
1
mix.exs
|
@ -50,7 +50,6 @@ defmodule Entendu.MixProject do
|
||||||
{:libcluster, "~> 3.2"},
|
{:libcluster, "~> 3.2"},
|
||||||
{:ueberauth, "~> 0.7.0"},
|
{:ueberauth, "~> 0.7.0"},
|
||||||
{:ueberauth_github, "~> 0.8.1"},
|
{:ueberauth_github, "~> 0.8.1"},
|
||||||
{:ueberauth_google, "~> 0.10.1"},
|
|
||||||
{:react_phoenix, "~> 1.3"},
|
{:react_phoenix, "~> 1.3"},
|
||||||
{:params, "~> 2.2"},
|
{:params, "~> 2.2"},
|
||||||
{:waffle, "~> 1.1"},
|
{:waffle, "~> 1.1"},
|
||||||
|
|
1
mix.lock
1
mix.lock
|
@ -41,7 +41,6 @@
|
||||||
"telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"},
|
"telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"},
|
||||||
"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"},
|
||||||
"ueberauth_google": {:hex, :ueberauth_google, "0.10.1", "db7bd2d99d2ff38e7449042a08d9560741b0dcaf1c31191729b97188b025465e", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7.0", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "b799f547d279bb836e1f7039fc9fbb3a9d008a695e2a25bd06bffe591a168ba1"},
|
|
||||||
"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": {: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"},
|
"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"},
|
||||||
|
|
Loading…
Reference in New Issue