Merge branch 'instance-docs' into 'develop'
AdminAPI: Allow to modify Terms of Service and Instance Panel via Admin API Closes #1516 See merge request pleroma/pleroma!2931
This commit is contained in:
commit
608824b97e
8 changed files with 377 additions and 0 deletions
|
@ -42,6 +42,12 @@ switched to a new configuration mechanism, however it was not officially removed
|
||||||
### Added
|
### Added
|
||||||
- Rich media failure tracking (along with `:failure_backoff` option).
|
- Rich media failure tracking (along with `:failure_backoff` option).
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Admin API Changes</summary>
|
||||||
|
|
||||||
|
- Add `PATCH /api/pleroma/admin/instance_document/:document_name` to modify the Terms of Service and Instance Panel HTML pages via Admin API
|
||||||
|
</details>
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Default HTTP adapter not respecting pool setting, leading to possible OOM.
|
- Default HTTP adapter not respecting pool setting, leading to possible OOM.
|
||||||
- Fixed uploading webp images when the Exiftool Upload Filter is enabled by skipping them
|
- Fixed uploading webp images when the Exiftool Upload Filter is enabled by skipping them
|
||||||
|
|
|
@ -1455,3 +1455,45 @@ Loads json generated from `config/descriptions.exs`.
|
||||||
"unread": false
|
"unread": false
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `GET /api/pleroma/admin/instance_document/:document_name`
|
||||||
|
|
||||||
|
### Get an instance document
|
||||||
|
|
||||||
|
- Authentication: required
|
||||||
|
|
||||||
|
- Response:
|
||||||
|
|
||||||
|
Returns the content of the document
|
||||||
|
|
||||||
|
```html
|
||||||
|
<h1>Instance panel</h1>
|
||||||
|
```
|
||||||
|
|
||||||
|
## `PATCH /api/pleroma/admin/instance_document/:document_name`
|
||||||
|
- Params:
|
||||||
|
- `file` (the file to be uploaded, using multipart form data.)
|
||||||
|
|
||||||
|
### Update an instance document
|
||||||
|
|
||||||
|
- Authentication: required
|
||||||
|
|
||||||
|
- Response:
|
||||||
|
|
||||||
|
``` json
|
||||||
|
{
|
||||||
|
"url": "https://example.com/instance/panel.html"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `DELETE /api/pleroma/admin/instance_document/:document_name`
|
||||||
|
|
||||||
|
### Delete an instance document
|
||||||
|
|
||||||
|
- Response:
|
||||||
|
|
||||||
|
``` json
|
||||||
|
{
|
||||||
|
"url": "https://example.com/instance/panel.html"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.AdminAPI.InstanceDocumentController do
|
||||||
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
|
alias Pleroma.Plugs.InstanceStatic
|
||||||
|
alias Pleroma.Plugs.OAuthScopesPlug
|
||||||
|
alias Pleroma.Web.InstanceDocument
|
||||||
|
|
||||||
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
|
|
||||||
|
action_fallback(Pleroma.Web.AdminAPI.FallbackController)
|
||||||
|
|
||||||
|
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.InstanceDocumentOperation
|
||||||
|
|
||||||
|
plug(OAuthScopesPlug, %{scopes: ["read"], admin: true} when action == :show)
|
||||||
|
plug(OAuthScopesPlug, %{scopes: ["write"], admin: true} when action in [:update, :delete])
|
||||||
|
|
||||||
|
def show(conn, %{name: document_name}) do
|
||||||
|
with {:ok, url} <- InstanceDocument.get(document_name),
|
||||||
|
{:ok, content} <- File.read(InstanceStatic.file_path(url)) do
|
||||||
|
conn
|
||||||
|
|> put_resp_content_type("text/html")
|
||||||
|
|> send_resp(200, content)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update(%{body_params: %{file: file}} = conn, %{name: document_name}) do
|
||||||
|
with {:ok, url} <- InstanceDocument.put(document_name, file.path) do
|
||||||
|
json(conn, %{"url" => url})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete(conn, %{name: document_name}) do
|
||||||
|
with :ok <- InstanceDocument.delete(document_name) do
|
||||||
|
json(conn, %{})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,115 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.ApiSpec.Admin.InstanceDocumentOperation do
|
||||||
|
alias OpenApiSpex.Operation
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
alias Pleroma.Web.ApiSpec.Helpers
|
||||||
|
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||||
|
|
||||||
|
def open_api_operation(action) do
|
||||||
|
operation = String.to_existing_atom("#{action}_operation")
|
||||||
|
apply(__MODULE__, operation, [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Admin", "InstanceDocument"],
|
||||||
|
summary: "Get the instance document",
|
||||||
|
operationId: "AdminAPI.InstanceDocumentController.show",
|
||||||
|
security: [%{"oAuth" => ["read"]}],
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(:name, :path, %Schema{type: :string}, "The document name",
|
||||||
|
required: true
|
||||||
|
)
|
||||||
|
| Helpers.admin_api_params()
|
||||||
|
],
|
||||||
|
responses: %{
|
||||||
|
200 => document_content(),
|
||||||
|
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||||
|
403 => Operation.response("Forbidden", "application/json", ApiError),
|
||||||
|
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Admin", "InstanceDocument"],
|
||||||
|
summary: "Update the instance document",
|
||||||
|
operationId: "AdminAPI.InstanceDocumentController.update",
|
||||||
|
security: [%{"oAuth" => ["write"]}],
|
||||||
|
requestBody: Helpers.request_body("Parameters", update_request()),
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(:name, :path, %Schema{type: :string}, "The document name",
|
||||||
|
required: true
|
||||||
|
)
|
||||||
|
| Helpers.admin_api_params()
|
||||||
|
],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("InstanceDocument", "application/json", instance_document()),
|
||||||
|
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||||
|
403 => Operation.response("Forbidden", "application/json", ApiError),
|
||||||
|
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp update_request do
|
||||||
|
%Schema{
|
||||||
|
title: "UpdateRequest",
|
||||||
|
description: "POST body for uploading the file",
|
||||||
|
type: :object,
|
||||||
|
required: [:file],
|
||||||
|
properties: %{
|
||||||
|
file: %Schema{
|
||||||
|
type: :string,
|
||||||
|
format: :binary,
|
||||||
|
description: "The file to be uploaded, using multipart form data."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Admin", "InstanceDocument"],
|
||||||
|
summary: "Get the instance document",
|
||||||
|
operationId: "AdminAPI.InstanceDocumentController.delete",
|
||||||
|
security: [%{"oAuth" => ["write"]}],
|
||||||
|
parameters: [
|
||||||
|
Operation.parameter(:name, :path, %Schema{type: :string}, "The document name",
|
||||||
|
required: true
|
||||||
|
)
|
||||||
|
| Helpers.admin_api_params()
|
||||||
|
],
|
||||||
|
responses: %{
|
||||||
|
200 => Operation.response("InstanceDocument", "application/json", instance_document()),
|
||||||
|
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||||
|
403 => Operation.response("Forbidden", "application/json", ApiError),
|
||||||
|
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp instance_document do
|
||||||
|
%Schema{
|
||||||
|
title: "InstanceDocument",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
url: %Schema{type: :string}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
"url" => "https://example.com/static/terms-of-service.html"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp document_content do
|
||||||
|
Operation.response("InstanceDocumentContent", "text/html", %Schema{
|
||||||
|
type: :string,
|
||||||
|
example: "<h1>Instance panel</h1>"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
62
lib/pleroma/web/instance_document.ex
Normal file
62
lib/pleroma/web/instance_document.ex
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.InstanceDocument do
|
||||||
|
alias Pleroma.Config
|
||||||
|
alias Pleroma.Web.Endpoint
|
||||||
|
|
||||||
|
@instance_documents %{
|
||||||
|
"terms-of-service" => "/static/terms-of-service.html",
|
||||||
|
"instance-panel" => "/instance/panel.html"
|
||||||
|
}
|
||||||
|
|
||||||
|
@spec get(String.t()) :: {:ok, String.t()} | {:error, atom()}
|
||||||
|
def get(document_name) do
|
||||||
|
case Map.fetch(@instance_documents, document_name) do
|
||||||
|
{:ok, path} -> {:ok, path}
|
||||||
|
_ -> {:error, :not_found}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec put(String.t(), String.t()) :: {:ok, String.t()} | {:error, atom()}
|
||||||
|
def put(document_name, origin_path) do
|
||||||
|
with {_, {:ok, destination_path}} <-
|
||||||
|
{:instance_document, Map.fetch(@instance_documents, document_name)},
|
||||||
|
:ok <- put_file(origin_path, destination_path) do
|
||||||
|
{:ok, Path.join(Endpoint.url(), destination_path)}
|
||||||
|
else
|
||||||
|
{:instance_document, :error} -> {:error, :not_found}
|
||||||
|
error -> error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec delete(String.t()) :: :ok | {:error, atom()}
|
||||||
|
def delete(document_name) do
|
||||||
|
with {_, {:ok, path}} <- {:instance_document, Map.fetch(@instance_documents, document_name)},
|
||||||
|
instance_static_dir_path <- instance_static_dir(path),
|
||||||
|
:ok <- File.rm(instance_static_dir_path) do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
{:instance_document, :error} -> {:error, :not_found}
|
||||||
|
{:error, :enoent} -> {:error, :not_found}
|
||||||
|
error -> error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp put_file(origin_path, destination_path) do
|
||||||
|
with destination <- instance_static_dir(destination_path),
|
||||||
|
{_, :ok} <- {:mkdir_p, File.mkdir_p(Path.dirname(destination))},
|
||||||
|
{_, {:ok, _}} <- {:copy, File.copy(origin_path, destination)} do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
{error, _} -> {:error, error}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp instance_static_dir(filename) do
|
||||||
|
[:instance, :static_dir]
|
||||||
|
|> Config.get!()
|
||||||
|
|> Path.join(filename)
|
||||||
|
end
|
||||||
|
end
|
|
@ -182,6 +182,10 @@ defmodule Pleroma.Web.Router do
|
||||||
|
|
||||||
get("/instances/:instance/statuses", AdminAPIController, :list_instance_statuses)
|
get("/instances/:instance/statuses", AdminAPIController, :list_instance_statuses)
|
||||||
|
|
||||||
|
get("/instance_document/:name", InstanceDocumentController, :show)
|
||||||
|
patch("/instance_document/:name", InstanceDocumentController, :update)
|
||||||
|
delete("/instance_document/:name", InstanceDocumentController, :delete)
|
||||||
|
|
||||||
patch("/users/confirm_email", AdminAPIController, :confirm_email)
|
patch("/users/confirm_email", AdminAPIController, :confirm_email)
|
||||||
patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
|
patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
|
||||||
|
|
||||||
|
|
1
test/fixtures/custom_instance_panel.html
vendored
Normal file
1
test/fixtures/custom_instance_panel.html
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<h2>Custom instance panel</h2>
|
|
@ -0,0 +1,106 @@
|
||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.AdminAPI.InstanceDocumentControllerTest do
|
||||||
|
use Pleroma.Web.ConnCase, async: true
|
||||||
|
import Pleroma.Factory
|
||||||
|
alias Pleroma.Config
|
||||||
|
|
||||||
|
@dir "test/tmp/instance_static"
|
||||||
|
@default_instance_panel ~s(<p>Welcome to <a href="https://pleroma.social" target="_blank">Pleroma!</a></p>)
|
||||||
|
|
||||||
|
setup do
|
||||||
|
File.mkdir_p!(@dir)
|
||||||
|
on_exit(fn -> File.rm_rf(@dir) end)
|
||||||
|
end
|
||||||
|
|
||||||
|
setup do: clear_config([:instance, :static_dir], @dir)
|
||||||
|
|
||||||
|
setup do
|
||||||
|
admin = insert(:user, is_admin: true)
|
||||||
|
token = insert(:oauth_admin_token, user: admin)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> assign(:user, admin)
|
||||||
|
|> assign(:token, token)
|
||||||
|
|
||||||
|
{:ok, %{admin: admin, token: token, conn: conn}}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "GET /api/pleroma/admin/instance_document/:name" do
|
||||||
|
test "return the instance document url", %{conn: conn} do
|
||||||
|
conn = get(conn, "/api/pleroma/admin/instance_document/instance-panel")
|
||||||
|
|
||||||
|
assert content = html_response(conn, 200)
|
||||||
|
assert String.contains?(content, @default_instance_panel)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it returns 403 if requested by a non-admin" do
|
||||||
|
non_admin_user = insert(:user)
|
||||||
|
token = insert(:oauth_token, user: non_admin_user)
|
||||||
|
|
||||||
|
conn =
|
||||||
|
build_conn()
|
||||||
|
|> assign(:user, non_admin_user)
|
||||||
|
|> assign(:token, token)
|
||||||
|
|> get("/api/pleroma/admin/instance_document/instance-panel")
|
||||||
|
|
||||||
|
assert json_response(conn, :forbidden)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it returns 404 if the instance document with the given name doesn't exist", %{
|
||||||
|
conn: conn
|
||||||
|
} do
|
||||||
|
conn = get(conn, "/api/pleroma/admin/instance_document/1234")
|
||||||
|
|
||||||
|
assert json_response_and_validate_schema(conn, 404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "PATCH /api/pleroma/admin/instance_document/:name" do
|
||||||
|
test "uploads the instance document", %{conn: conn} do
|
||||||
|
image = %Plug.Upload{
|
||||||
|
content_type: "text/html",
|
||||||
|
path: Path.absname("test/fixtures/custom_instance_panel.html"),
|
||||||
|
filename: "custom_instance_panel.html"
|
||||||
|
}
|
||||||
|
|
||||||
|
conn =
|
||||||
|
conn
|
||||||
|
|> put_req_header("content-type", "multipart/form-data")
|
||||||
|
|> patch("/api/pleroma/admin/instance_document/instance-panel", %{
|
||||||
|
"file" => image
|
||||||
|
})
|
||||||
|
|
||||||
|
assert %{"url" => url} = json_response_and_validate_schema(conn, 200)
|
||||||
|
index = get(build_conn(), url)
|
||||||
|
assert html_response(index, 200) == "<h2>Custom instance panel</h2>"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "DELETE /api/pleroma/admin/instance_document/:name" do
|
||||||
|
test "deletes the instance document", %{conn: conn} do
|
||||||
|
File.mkdir!(@dir <> "/instance/")
|
||||||
|
File.write!(@dir <> "/instance/panel.html", "Custom instance panel")
|
||||||
|
|
||||||
|
conn_resp =
|
||||||
|
conn
|
||||||
|
|> get("/api/pleroma/admin/instance_document/instance-panel")
|
||||||
|
|
||||||
|
assert html_response(conn_resp, 200) == "Custom instance panel"
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> delete("/api/pleroma/admin/instance_document/instance-panel")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
conn_resp =
|
||||||
|
conn
|
||||||
|
|> get("/api/pleroma/admin/instance_document/instance-panel")
|
||||||
|
|
||||||
|
assert content = html_response(conn_resp, 200)
|
||||||
|
assert String.contains?(content, @default_instance_panel)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue