4 Commits

9 changed files with 149 additions and 31 deletions

View File

@@ -1,6 +1,13 @@
type IntendedUser = { type IntendedUser = {
name: string; name: string;
emails: string[]; emails: OAuthEmail[];
username: string;
};
type OAuthEmail = {
email: string;
primary: boolean;
verified: boolean;
}; };
type IntendedLink = { type IntendedLink = {

View File

@@ -20,6 +20,7 @@ type AuthPageProps = {
service: string; service: string;
recipient: string; recipient: string;
user: IntendedUser | null; user: IntendedUser | null;
error: string;
}; };
interface Keys { interface Keys {
@@ -40,6 +41,7 @@ const AuthPage = (props: AuthPageProps) => {
const [secretFileUrl, setSecretFileUrl] = useState<string>("#"); const [secretFileUrl, setSecretFileUrl] = useState<string>("#");
const [secretFileName, setSecretFileName] = useState<string>(""); const [secretFileName, setSecretFileName] = useState<string>("");
const [secretMessage, setSecretMessage] = useState<string>("Decrypting..."); const [secretMessage, setSecretMessage] = useState<string>("Decrypting...");
const [messageRevealed, setMessageRevealed] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
init().catch((reason) => { init().catch((reason) => {
@@ -47,6 +49,9 @@ const AuthPage = (props: AuthPageProps) => {
}); });
}, []); }, []);
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 link: LinkFiles | null = await retrieveLink();
const keys: Keys | null = await retrieveKeys(); const keys: Keys | null = await retrieveKeys();
@@ -55,6 +60,14 @@ const AuthPage = (props: AuthPageProps) => {
} }
}; };
const userEmails = (): string[] => {
return user
? user.emails
.filter((email) => email.verified)
.map((email) => email.email)
: [];
};
const retrieveLink = async (): Promise<LinkFiles | null> => { 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();
@@ -135,6 +148,7 @@ 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) {
@@ -153,21 +167,57 @@ const AuthPage = (props: AuthPageProps) => {
type: link.filetype ? link.filetype : "text/plain", type: link.filetype ? link.filetype : "text/plain",
}); });
setSecretFileUrl(window.URL.createObjectURL(blob)); 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 => { const renderHeader = (): JSX.Element => {
return ( return (
<div> <div>
<Header2 style={{ margin: ".4rem" }}>Someone sent you a secret</Header2> <Header2 style={{ margin: ".4rem" }}>
{user ? "You have been identified!" : "Someone sent you a secret"}
</Header2>
{user ? ( {user ? (
<Header3 small> <Header3 small>
Hello {user.name}, you are logged in {service} which has the {messageRevealed
following verified emails: {user.emails.join(", ")} ? "The following message and/or file is for your eyes only."
: "Unfortunately, you are not the intended recipient."}
<br />
<br />
</Header3> </Header3>
) : ( ) : (
<Header3 small> <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> </Header3>
)} )}
</div> </div>
@@ -232,11 +282,13 @@ const AuthPage = (props: AuthPageProps) => {
/> />
</a> </a>
<Spacer space="3rem" /> <Spacer space="3rem" />
<a href={`https://intended.link/auth/${service}`}> <a href={`https://intended.link/auth/logout`}>
<Button variant="primary" wide onClick={() => {}}> <Button variant="primary" wide onClick={() => {}}>
Re-Verify Logout
</Button> </Button>
</a> </a>
<Spacer space="3rem" />
{renderFooter()}
</CenteredContainer> </CenteredContainer>
</CenteredContainer> </CenteredContainer>
); );

View File

@@ -88,6 +88,9 @@ const JustPage = (props: JustPageProps) => {
form.append("file_content", blobData, fileName); form.append("file_content", blobData, fileName);
form.append("filename", fileName); form.append("filename", fileName);
form.append("filetype", fileType); form.append("filetype", fileType);
HexMix.arrayBufferToString(encrypted, (result: string) => {
sessionStorage.setItem("encoded_file", result);
});
}; };
const textFormData = async (form: FormData, aesKey: AESKey) => { const textFormData = async (form: FormData, aesKey: AESKey) => {
@@ -102,6 +105,9 @@ const JustPage = (props: JustPageProps) => {
); );
const blobData = new Blob([encrypted]); const blobData = new Blob([encrypted]);
form.append("text_content", blobData, "secret_message.txt"); form.append("text_content", blobData, "secret_message.txt");
HexMix.arrayBufferToString(encrypted, (result: string) => {
sessionStorage.setItem("encoded_message", result);
});
}; };
const createKey = async (): Promise<AESKey> => { const createKey = async (): Promise<AESKey> => {

View File

@@ -1,8 +1,30 @@
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 ( return (
<React.StrictMode> <React.StrictMode>
<GlobalStyle /> <GlobalStyle />
@@ -15,7 +37,11 @@ const SplashPage = () => {
and secretly. and secretly.
</Header3> </Header3>
<Spacer /> <Spacer />
<Button variant="secondary" boldFont onClick={() => window.location.href = "/just"}> <Button
variant="secondary"
boldFont
onClick={() => (window.location.href = "/just")}
>
START SHARING START SHARING
</Button> </Button>
</CenteredContainer> </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 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");
@@ -11,7 +32,11 @@ const YouPage = () => {
return `${window.location.origin}/just/for/you/${linkId}#${keyHex}.${ivHex}`; 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 () => { const copyUrl = async () => {
try { try {
@@ -19,7 +44,7 @@ const YouPage = () => {
} 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>
@@ -47,20 +72,23 @@ const YouPage = () => {
looking eh?: looking eh?:
</Label> </Label>
</TextAlignWrapper> </TextAlignWrapper>
<Input <Input variant="disabled-light" id="encodedSecret" value={encoded} />
variant="disabled-light"
id="encodedSecret"
value={url}
/>
<Spacer space="3rem" /> <Spacer space="3rem" />
<SpaceBetweenContainer> <SpaceBetweenContainer>
<Button variant="secondary" onClick={() => window.location.href = "/just/for"}> <Button
variant="secondary"
onClick={() => (window.location.href = "/just/for")}
>
<IconArrow arrowDirection="left" /> <IconArrow arrowDirection="left" />
</Button> </Button>
<Button onClick={() => { <Button
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>

View File

@@ -40,9 +40,9 @@ defmodule EntenduWeb.Router do
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

View File

@@ -1,5 +1,3 @@
<main role="main"> <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>
<%= @inner_content %> <%= @inner_content %>
</main> </main>

View File

@@ -3,5 +3,6 @@
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)
}) %> }) %>

View File

@@ -1,6 +1,6 @@
<section> <section>
<%= react_component("Components.SplashPage") %> <%= react_component("Components.SplashPage", %{ error: get_flash(@conn, :error) }) %>
<%= if @current_user do %> <%= if @current_user do %>
<h2>Welcome, <%= @current_user.name %>!</h2> <h2>Welcome, <%= @current_user.name %>!</h2>