From 82734947f152408341549d6076726c1fd1316283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tao=20Bror=20Bojl=C3=A9n?= Date: Fri, 23 Aug 2019 15:08:05 +0200 Subject: [PATCH] add AP login for instance admins --- CHANGELOG.md | 2 ++ backend/config/config.exs | 4 +++ backend/lib/backend/api.ex | 6 ++-- .../lib/backend/crawler/crawlers/mastodon.ex | 19 +++++----- .../lib/backend/crawler/crawlers/misskey.ex | 4 +-- .../controllers/admin_controller.ex | 2 +- .../controllers/admin_login_controller.ex | 36 +++++++++++++++---- .../lib/backend_web/views/admin_login_view.ex | 12 +++++-- backend/lib/mastodon/Messenger.ex | 22 ++++++++++++ backend/mix.exs | 4 ++- backend/mix.lock | 3 +- .../src/components/screens/AdminScreen.tsx | 5 +++ .../src/components/screens/LoginScreen.tsx | 20 +++++++---- 13 files changed, 106 insertions(+), 33 deletions(-) create mode 100644 backend/lib/mastodon/Messenger.ex diff --git a/CHANGELOG.md b/CHANGELOG.md index dc225de..7d3a717 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add support for logging in via an ActivityPub direct message to the instance admin. + ### Changed ### Deprecated diff --git a/backend/config/config.exs b/backend/config/config.exs index 528b54c..b43791d 100644 --- a/backend/config/config.exs +++ b/backend/config/config.exs @@ -51,6 +51,10 @@ config :backend, Backend.Mailer, adapter: Swoosh.Adapters.Sendgrid, api_key: System.get_env("SENDGRID_API_KEY") +config :backend, Mastodon.Messenger, + domain: System.get_env("MASTODON_DOMAIN"), + token: System.get_env("MASTODON_TOKEN") + config :backend, :crawler, status_age_limit_days: 28, status_count_limit: 5000, diff --git a/backend/lib/backend/api.ex b/backend/lib/backend/api.ex index 7ce5bea..8ed3db0 100644 --- a/backend/lib/backend/api.ex +++ b/backend/lib/backend/api.ex @@ -6,10 +6,10 @@ defmodule Backend.Api do import Backend.Util import Ecto.Query - @spec get_instance!(String.t()) :: Instance.t() - def get_instance!(domain) do + @spec get_instance(String.t()) :: Instance.t() | nil + def get_instance(domain) do Instance - |> Repo.get_by!(domain: domain) + |> Repo.get_by(domain: domain) end @spec get_instance_with_peers(String.t()) :: Instance.t() | nil diff --git a/backend/lib/backend/crawler/crawlers/mastodon.ex b/backend/lib/backend/crawler/crawlers/mastodon.ex index 8f50c30..74b6b03 100644 --- a/backend/lib/backend/crawler/crawlers/mastodon.ex +++ b/backend/lib/backend/crawler/crawlers/mastodon.ex @@ -45,10 +45,10 @@ defmodule Backend.Crawler.Crawlers.Mastodon do Map.take(instance["stats"], ["user_count"]) |> convert_keys_to_atoms(), %{ + instance_type: get_instance_type(instance), peers: [], interactions: %{}, statuses_seen: 0, - instance_type: nil, description: nil, version: nil, status_count: nil @@ -71,13 +71,6 @@ defmodule Backend.Crawler.Crawlers.Mastodon do } mentions in #{statuses_seen} statuses." ) - instance_type = - cond do - Map.get(instance, "version") |> String.downcase() =~ "pleroma" -> :pleroma - is_gab?(instance) -> :gab - true -> :mastodon - end - Map.merge( Map.merge( Map.take(instance, ["version", "description"]), @@ -88,7 +81,7 @@ defmodule Backend.Crawler.Crawlers.Mastodon do peers: peers, interactions: interactions, statuses_seen: statuses_seen, - instance_type: instance_type + instance_type: get_instance_type(instance) } ) end @@ -240,4 +233,12 @@ defmodule Backend.Crawler.Crawlers.Mastodon do title_is_gab end end + + defp get_instance_type(instance_stats) do + cond do + Map.get(instance_stats, "version") |> String.downcase() =~ "pleroma" -> :pleroma + is_gab?(instance_stats) -> :gab + true -> :mastodon + end + end end diff --git a/backend/lib/backend/crawler/crawlers/misskey.ex b/backend/lib/backend/crawler/crawlers/misskey.ex index 191f003..0a7c6c3 100644 --- a/backend/lib/backend/crawler/crawlers/misskey.ex +++ b/backend/lib/backend/crawler/crawlers/misskey.ex @@ -42,14 +42,14 @@ defmodule Backend.Crawler.Crawlers.Misskey do crawl_large_instance(domain, user_count, status_count) else %{ + instance_type: :misskey, version: nil, description: nil, user_count: user_count, status_count: nil, peers: [], interactions: %{}, - statuses_seen: 0, - instance_type: nil + statuses_seen: 0 } end end diff --git a/backend/lib/backend_web/controllers/admin_controller.ex b/backend/lib/backend_web/controllers/admin_controller.ex index 784817a..221e35d 100644 --- a/backend/lib/backend_web/controllers/admin_controller.ex +++ b/backend/lib/backend_web/controllers/admin_controller.ex @@ -8,7 +8,7 @@ defmodule BackendWeb.AdminController do [token] = get_req_header(conn, "token") with {:ok, domain} <- Auth.verify_token(token) do - instance = Api.get_instance!(domain) + instance = Api.get_instance(domain) render(conn, "show.json", instance: instance) end end diff --git a/backend/lib/backend_web/controllers/admin_login_controller.ex b/backend/lib/backend_web/controllers/admin_login_controller.ex index 2ef16e3..a1c54fa 100644 --- a/backend/lib/backend_web/controllers/admin_login_controller.ex +++ b/backend/lib/backend_web/controllers/admin_login_controller.ex @@ -1,7 +1,9 @@ defmodule BackendWeb.AdminLoginController do use BackendWeb, :controller import Backend.Util + alias Backend.Api alias Backend.Mailer.UserEmail + alias Mastodon.Messenger action_fallback BackendWeb.FallbackController @@ -11,20 +13,38 @@ defmodule BackendWeb.AdminLoginController do """ def show(conn, %{"id" => domain}) do cleaned_domain = clean_domain(domain) + instance = Api.get_instance(domain) - instance_data = get_and_decode!("https://#{cleaned_domain}/api/v1/instance") + keyword_args = + cond do + instance == nil or instance.type == nil -> + [error: "We have not seen this instance before. Please check for typos."] - render(conn, "show.json", instance_data: instance_data, cleaned_domain: cleaned_domain) + not Enum.member?(["mastodon", "pleroma", "gab"], instance.type) -> + [error: "It is only possible to administer Mastodon and Pleroma instances."] + + true -> + case get_and_decode("https://#{cleaned_domain}/api/v1/instance") do + {:ok, instance_data} -> + [instance_data: instance_data, cleaned_domain: cleaned_domain] + + {:error, _err} -> + [error: "Unable to get instance details. Is it currently live?"] + end + end + + render(conn, "show.json", keyword_args) end def create(conn, %{"domain" => domain, "type" => type}) do cleaned_domain = clean_domain(domain) + {data_state, instance_data} = get_and_decode("https://#{cleaned_domain}/api/v1/instance") - instance_data = get_and_decode!("https://#{cleaned_domain}/api/v1/instance") - - # credo:disable-for-lines:16 Credo.Check.Refactor.CondStatements error = cond do + data_state == :error -> + "Unable to get instance details. Is it currently live?" + type == "email" -> email = Map.get(instance_data, "email") @@ -33,8 +53,10 @@ defmodule BackendWeb.AdminLoginController do {:error, _} -> "Failed to send email." end - # type == "fediverseAccount" -> - # account = nil + type == "fediverseAccount" -> + username = get_in(instance_data, ["contact_account", "username"]) + _status = Messenger.dm_login_link(username, cleaned_domain) + nil true -> "Invalid account type. Must be 'email' or 'fediverseAccount'." diff --git a/backend/lib/backend_web/views/admin_login_view.ex b/backend/lib/backend_web/views/admin_login_view.ex index e2e550a..220aa39 100644 --- a/backend/lib/backend_web/views/admin_login_view.ex +++ b/backend/lib/backend_web/views/admin_login_view.ex @@ -2,9 +2,17 @@ defmodule BackendWeb.AdminLoginView do use BackendWeb, :view import Backend.Util - def render("show.json", %{instance_data: instance_data, cleaned_domain: cleaned_domain}) do - username = get_in(instance_data, ["contact_account", "username"]) + def render("show.json", %{error: error}) do + %{ + error: error + } + end + def render("show.json", %{ + instance_data: instance_data, + cleaned_domain: cleaned_domain + }) do + username = get_in(instance_data, ["contact_account", "username"]) fedi_account = get_account(username, cleaned_domain) %{ diff --git a/backend/lib/mastodon/Messenger.ex b/backend/lib/mastodon/Messenger.ex new file mode 100644 index 0000000..1d99966 --- /dev/null +++ b/backend/lib/mastodon/Messenger.ex @@ -0,0 +1,22 @@ +defmodule Mastodon.Messenger do + import Backend.{Auth, Util} + require Logger + + def dm_login_link(username, user_domain) do + mastodon_domain = Application.get_env(:backend, __MODULE__)[:domain] + token = Application.get_env(:backend, __MODULE__)[:token] + frontend_domain = get_config(:frontend_domain) + + conn = Hunter.new(base_url: "https://#{mastodon_domain}", bearer_token: token) + Logger.info(inspect(conn)) + + status_text = + "@#{username}@#{user_domain} " <> + "Someone tried to log in to #{user_domain} on https://#{frontend_domain}.\n" <> + "If it was you, click here to confirm:\n" <> + "#{get_login_link(user_domain)} " <> + "This link will be valid for 12 hours." + + Hunter.Status.create_status(conn, status_text, visibility: :direct) + end +end diff --git a/backend/mix.exs b/backend/mix.exs index 4a5bd65..e0f9626 100644 --- a/backend/mix.exs +++ b/backend/mix.exs @@ -64,7 +64,9 @@ defmodule Backend.MixProject do {:elasticsearch, "~> 1.0"}, {:appsignal, "~> 1.10.1"}, {:credo, "~> 1.1", only: [:dev, :test], runtime: false}, - {:nebulex, "~> 1.1"} + {:nebulex, "~> 1.1"}, + {:hunter, "~> 0.5.1"}, + {:poison, "~> 4.0", override: true} ] end diff --git a/backend/mix.lock b/backend/mix.lock index 44778e5..406198d 100644 --- a/backend/mix.lock +++ b/backend/mix.lock @@ -26,6 +26,7 @@ "hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "honeydew": {:hex, :honeydew, "1.4.3", "f2d976aaf8b9b914a635d2d483f1a71d2f6d8651809474dd5db581953cbebb30", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "httpoison": {:hex, :httpoison, "1.5.1", "0f55b5b673b03c5c327dac7015a67cb571b99b631acc0bc1b0b98dcd6b9f2104", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "hunter": {:hex, :hunter, "0.5.1", "374dc4a800e2c340659657f8875e466075c7ea532e0d7a7787665f272b410150", [:mix], [{:httpoison, "~> 1.5", [hex: :httpoison, repo: "hexpm", optional: false]}, {:poison, "~> 4.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "idna": {:hex, :idna, "5.1.2", "e21cb58a09f0228a9e0b95eaa1217f1bcfc31a1aaa6e1fdf2f53a33f7dbd9494", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, "inflex": {:hex, :inflex, "1.10.0", "8366a7696e70e1813aca102e61274addf85d99f4a072b2f9c7984054ea1b9d29", [:mix], [], "hexpm"}, "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, @@ -44,7 +45,7 @@ "plug": {:hex, :plug, "1.8.2", "0bcce1daa420f189a6491f3940cc77ea7fb1919761175c9c3b59800d897440fc", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"}, "plug_cowboy": {:hex, :plug_cowboy, "2.1.0", "b75768153c3a8a9e8039d4b25bb9b14efbc58e9c4a6e6a270abff1cd30cbe320", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"}, - "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, + "poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm"}, "postgrex": {:hex, :postgrex, "0.14.3", "5754dee2fdf6e9e508cbf49ab138df964278700b764177e8f3871e658b345a1e", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, "public_suffix": {:hex, :public_suffix, "0.6.0", "100cfe86f13f9f6f0cf67e743b1b83c78dd1223a2c422fa03ebf4adff514cbc3", [:mix], [{:idna, ">= 1.2.0 and < 6.0.0", [hex: :idna, repo: "hexpm", optional: false]}], "hexpm"}, "quantum": {:hex, :quantum, "2.3.4", "72a0e8855e2adc101459eac8454787cb74ab4169de6ca50f670e72142d4960e9", [:mix], [{:calendar, "~> 0.17", [hex: :calendar, repo: "hexpm", optional: true]}, {:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.12", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:swarm, "~> 3.3", [hex: :swarm, repo: "hexpm", optional: false]}, {:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: true]}], "hexpm"}, diff --git a/frontend/src/components/screens/AdminScreen.tsx b/frontend/src/components/screens/AdminScreen.tsx index 423561e..feb122a 100644 --- a/frontend/src/components/screens/AdminScreen.tsx +++ b/frontend/src/components/screens/AdminScreen.tsx @@ -54,6 +54,7 @@ class AdminScreen extends React.PureComponent { unsetAuthToken(); + AppToaster.show({ + icon: IconNames.LOG_OUT, + message: "Logged out." + }); this.props.navigate("/admin/login"); }; } diff --git a/frontend/src/components/screens/LoginScreen.tsx b/frontend/src/components/screens/LoginScreen.tsx index 7e5b334..410d4d3 100644 --- a/frontend/src/components/screens/LoginScreen.tsx +++ b/frontend/src/components/screens/LoginScreen.tsx @@ -3,6 +3,7 @@ import { IconNames } from "@blueprintjs/icons"; import React from "react"; import { Redirect } from "react-router"; import styled from "styled-components"; +import AppToaster from "../../toaster"; import { getAuthToken, getFromApi, postToApi } from "../../util"; import { Page } from "../atoms"; import { ErrorState } from "../molecules"; @@ -118,7 +119,7 @@ class LoginScreen extends React.PureComponent<{}, ILoginScreenState> { return; } const loginWithEmail = () => this.login("email"); - // const loginWithDm = () => this.login("fediverseAccount"); + const loginWithDm = () => this.login("fediverseAccount"); return ( <>

Choose an authentication method

@@ -133,7 +134,7 @@ class LoginScreen extends React.PureComponent<{}, ILoginScreenState> { {`Email ${loginTypes.email}`} )} - {/* {loginTypes.fediverseAccount && ( + {loginTypes.fediverseAccount && ( { > {`DM ${loginTypes.fediverseAccount}`} - )} */} + )} ); @@ -179,15 +180,20 @@ class LoginScreen extends React.PureComponent<{}, ILoginScreenState> { } getFromApi(`admin/login/${domain}`) .then(response => { - if ("error" in response || "errors" in response) { + if (!!response.error) { // Go to catch() below - throw new Error(); + throw new Error(response.error); } else { this.setState({ loginTypes: response, isGettingLoginTypes: false }); } }) - .catch(() => { - this.setState({ error: true }); + .catch((err: Error) => { + AppToaster.show({ + icon: IconNames.ERROR, + intent: Intent.DANGER, + message: err.message + }); + this.setState({ isGettingLoginTypes: false }); }); };