add AP login for instance admins

This commit is contained in:
Tao Bror Bojlén 2019-08-23 15:08:05 +02:00
parent 53bc0d3090
commit 82734947f1
No known key found for this signature in database
GPG Key ID: C6EC7AAB905F9E6F
13 changed files with 106 additions and 33 deletions

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'."

View File

@ -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)
%{

View File

@ -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

View File

@ -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

View File

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

View File

@ -54,6 +54,7 @@ class AdminScreen extends React.PureComponent<IAdminScreenProps, IAdminScreenSta
message: "Failed to load settings.",
timeout: 0
});
unsetAuthToken();
});
}
}
@ -158,6 +159,10 @@ class AdminScreen extends React.PureComponent<IAdminScreenProps, IAdminScreenSta
private logout = () => {
unsetAuthToken();
AppToaster.show({
icon: IconNames.LOG_OUT,
message: "Logged out."
});
this.props.navigate("/admin/login");
};
}

View File

@ -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 (
<>
<H4>Choose an authentication method</H4>
@ -133,7 +134,7 @@ class LoginScreen extends React.PureComponent<{}, ILoginScreenState> {
{`Email ${loginTypes.email}`}
</LoginTypeButton>
)}
{/* {loginTypes.fediverseAccount && (
{loginTypes.fediverseAccount && (
<LoginTypeButton
large={true}
icon={IconNames.GLOBE_NETWORK}
@ -142,7 +143,7 @@ class LoginScreen extends React.PureComponent<{}, ILoginScreenState> {
>
{`DM ${loginTypes.fediverseAccount}`}
</LoginTypeButton>
)} */}
)}
</LoginTypeContainer>
</>
);
@ -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 });
});
};