Merge pull request 'feature/gmail-oauth' (#27) from feature/gmail-oauth into master

Reviewed-on: #27
This commit is contained in:
silentsilas 2022-03-02 04:14:17 +00:00 committed by Intended
commit 6f1200fbd2
Failed to generate hash of commit
15 changed files with 3572 additions and 28 deletions

9
assets/jest.config.js Normal file
View File

@ -0,0 +1,9 @@
import type {Config} from '@jest/types';
// Sync object
const config: Config.InitialOptions = {
verbose: true,
transform: {
^.+\\.tsx?$: ts-jest,
},
};
export default config;

View File

@ -35,6 +35,11 @@ interface LinkFiles {
filetype: 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;
@ -61,13 +66,26 @@ const AuthPage = (props: AuthPageProps) => {
}; };
const userEmails = (): string[] => { const userEmails = (): string[] => {
if (!user?.emails) return [];
if (user.emails.length <= 0) return [];
return user return user
? user.emails ? user.emails
.filter((email) => email.verified) .filter(verifiedUserEmails)
.map((email) => email.email) .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 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();
@ -75,6 +93,10 @@ const AuthPage = (props: AuthPageProps) => {
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 linkResponse = await fetch(`/links/${linkId}`);
let linkData: IntendedLink | null; let linkData: IntendedLink | null;
@ -190,10 +212,13 @@ const AuthPage = (props: AuthPageProps) => {
small small
style={{ color: "#CCCCCC", fontSize: "1.4rem", textAlign: "left" }} style={{ color: "#CCCCCC", fontSize: "1.4rem", textAlign: "left" }}
> >
Hello {user.name}, you are logged in to{" "} Hello{user.name ? ` ${user.name}` : ""}! You are logged in to{" "}
<span style={{ color: "#A849CF" }}>{capitalize(service)}</span> as{" "} <span style={{ color: "#A849CF" }}>{capitalize(service)}</span>
<span style={{ color: "#32EFE7" }}>{user.username}</span>. This account {user.username ? " as " : ""}
has the following emails associated with it: <span style={{ color: "#32EFE7" }}>
{user.username ? `${user.username}` : ""}
</span>
. This account has the following emails associated with it:
<br /> <br />
<br /> <br />
<span style={{ color: "#32EFE7" }}>{userEmails().join(", ")}</span> <span style={{ color: "#32EFE7" }}>{userEmails().join(", ")}</span>

View File

@ -95,6 +95,7 @@ const ForPage = (props: ForPageProps) => {
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>

3489
assets/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,8 @@
"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.26",
@ -20,6 +21,7 @@
"@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",
@ -27,12 +29,14 @@
"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",

View File

@ -0,0 +1,3 @@
## Implementation Details
Write info on what we'd use, or just start adding in the tests and remove this file.

View File

@ -30,7 +30,9 @@ config :phoenix, :json_library, Jason
config :ueberauth, Ueberauth, config :ueberauth, Ueberauth,
providers: [ providers: [
github: {Ueberauth.Strategy.Github, [default_scope: "user:email", allow_private_emails: true]} github:
{Ueberauth.Strategy.Github, [default_scope: "user:email", allow_private_emails: true]},
google: {Ueberauth.Strategy.Google, [default_scope: "email"]}
] ]
config :waffle, config :waffle,

View File

@ -34,6 +34,10 @@ 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

View File

@ -6,7 +6,6 @@ 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)}
@ -67,11 +66,26 @@ defmodule Entendu.UserFromAuth do
end end
end end
def can_access?(recipient, %{emails: emails, username: username}), def can_access?(recipient, %{emails: emails, username: username} = stuff),
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: emails |> Enum.any?(&(&1["verified"] == true and &1["email"] == recipient)) do:
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

View File

@ -8,8 +8,6 @@ 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
@ -27,10 +25,8 @@ 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, recipient: recipient} <- link, with %{id: link_id} <- link,
{:ok, user} <- UserFromAuth.find_or_create(auth) do {:ok, user} <- UserFromAuth.find_or_create(auth) 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)

View File

@ -9,8 +9,6 @@ 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]
@ -18,7 +16,8 @@ defmodule EntenduWeb.LinkController do
action_fallback(FallbackController) action_fallback(FallbackController)
def just_page(conn, _params) do def just_page(conn, _params) do
render(conn, "just.html") conn
|> render("just.html")
end end
def just(conn, params) do def just(conn, params) do
@ -46,7 +45,7 @@ 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} = link <- Links.get_link(link_id) do with %Link{id: id, service: service, recipient: recipient} <- Links.get_link(link_id) do
conn conn
|> put_session(:intended_link, %{id: id, service: service, recipient: recipient}) |> put_session(:intended_link, %{id: id, service: service, recipient: recipient})
|> render("auth.html", %{intended_link: %{service: service, recipient: recipient}}) |> render("auth.html", %{intended_link: %{service: service, recipient: recipient}})

View File

@ -2,11 +2,9 @@ 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
@ -23,7 +21,7 @@ defmodule EntenduWeb.Plugs.AuthorizeLink do
if !user do if !user do
conn conn
|> put_status(403) |> put_status(403)
|> put_view(EntenduWeb.ErrorView) |> put_view(ErrorView)
|> render("error_code.json", message: "Unauthorized", code: 403) |> render("error_code.json", message: "Unauthorized", code: 403)
|> halt |> halt
else else
@ -35,21 +33,21 @@ defmodule EntenduWeb.Plugs.AuthorizeLink do
nil -> nil ->
conn conn
|> put_status(404) |> put_status(404)
|> put_view(EntenduWeb.ErrorView) |> put_view(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(EntenduWeb.ErrorView) |> put_view(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(EntenduWeb.ErrorView) |> put_view(ErrorView)
|> render("error_code.json", message: reason, code: 422) |> render("error_code.json", message: reason, code: 422)
|> halt |> halt
end end

View File

@ -36,7 +36,7 @@ defmodule Entendu.EncryptedLink do
# 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}" "priv/uploads/links/#{scope.id}"
end end

View File

@ -50,6 +50,7 @@ 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"},

View File

@ -41,6 +41,7 @@
"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"},