add AP login for instance admins
This commit is contained in:
parent
53bc0d3090
commit
82734947f1
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'."
|
||||
|
|
|
@ -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)
|
||||
|
||||
%{
|
||||
|
|
22
backend/lib/mastodon/Messenger.ex
Normal file
22
backend/lib/mastodon/Messenger.ex
Normal 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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"},
|
||||
|
|
|
@ -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");
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue