Merge pull request 'metrics' (#375) from stats into develop
Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/375
This commit is contained in:
commit
18bf82d747
3
.gitattributes
vendored
3
.gitattributes
vendored
|
@ -7,5 +7,4 @@
|
||||||
*.js.map binary
|
*.js.map binary
|
||||||
*.css binary
|
*.css binary
|
||||||
|
|
||||||
priv/static/instance/static.css diff=css
|
*.css diff=css
|
||||||
priv/static/static-fe/static-fe.css diff=css
|
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -76,3 +76,4 @@ docs/site
|
||||||
|
|
||||||
# docker stuff
|
# docker stuff
|
||||||
docker-db
|
docker-db
|
||||||
|
*.iml
|
||||||
|
|
|
@ -6,12 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Prometheus metrics exporting from `/api/v1/akkoma/metrics`
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
- Non-finch HTTP adapters
|
- Non-finch HTTP adapters
|
||||||
- Legacy redirect from /api/pleroma/admin to /api/v1/pleroma/admin
|
- Legacy redirect from /api/pleroma/admin to /api/v1/pleroma/admin
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Return HTTP error 413 when uploading an avatar or banner that's above the configured upload limit instead of a 500.
|
- Return HTTP error 413 when uploading an avatar or banner that's above the configured upload limit instead of a 500.
|
||||||
|
- Non-admin users now cannot register `admin` scope tokens (not security-critical, they didn't work before, but you _could_ create them)
|
||||||
|
|
||||||
### Upgrade notes
|
### Upgrade notes
|
||||||
- Ensure `config :tesla, :adapter` is either unset, or set to `{Tesla.Adapter.Finch, name: MyFinch}` in your .exs config
|
- Ensure `config :tesla, :adapter` is either unset, or set to `{Tesla.Adapter.Finch, name: MyFinch}` in your .exs config
|
||||||
|
|
|
@ -259,7 +259,8 @@
|
||||||
profile_directory: true,
|
profile_directory: true,
|
||||||
privileged_staff: false,
|
privileged_staff: false,
|
||||||
local_bubble: [],
|
local_bubble: [],
|
||||||
max_frontend_settings_json_chars: 100_000
|
max_frontend_settings_json_chars: 100_000,
|
||||||
|
export_prometheus_metrics: true
|
||||||
|
|
||||||
config :pleroma, :welcome,
|
config :pleroma, :welcome,
|
||||||
direct_message: [
|
direct_message: [
|
||||||
|
|
|
@ -964,6 +964,11 @@
|
||||||
type: {:list, :string},
|
type: {:list, :string},
|
||||||
description:
|
description:
|
||||||
"List of instances that make up your local bubble (closely-related instances). Used to populate the 'bubble' timeline (domain only)."
|
"List of instances that make up your local bubble (closely-related instances). Used to populate the 'bubble' timeline (domain only)."
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
key: :export_prometheus_metrics,
|
||||||
|
type: :boolean,
|
||||||
|
description: "Enable prometheus metrics (at /api/v1/akkoma/metrics)"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
33
docs/docs/administration/monitoring.md
Normal file
33
docs/docs/administration/monitoring.md
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# Monitoring Akkoma
|
||||||
|
|
||||||
|
If you run akkoma, you may be inclined to collect metrics to ensure your instance is running smoothly,
|
||||||
|
and that there's nothing quietly failing in the background.
|
||||||
|
|
||||||
|
To facilitate this, akkoma exposes prometheus metrics to be scraped.
|
||||||
|
|
||||||
|
## Prometheus
|
||||||
|
|
||||||
|
See: [export_prometheus_metrics](../configuration/cheatsheet#instance)
|
||||||
|
|
||||||
|
To scrape prometheus metrics, we need an oauth2 token with the `admin:metrics` scope.
|
||||||
|
|
||||||
|
consider using [constanze](https://akkoma.dev/AkkomaGang/constanze) to make this easier -
|
||||||
|
|
||||||
|
```bash
|
||||||
|
constanze token --client-app --scopes "admin:metrics" --client-name "Prometheus"
|
||||||
|
```
|
||||||
|
|
||||||
|
or see `scripts/create_metrics_app.sh` in the source tree for the process to get this token.
|
||||||
|
|
||||||
|
Once you have your token of the form `Bearer $ACCESS_TOKEN`, you can use that in your prometheus config:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- job_name: akkoma
|
||||||
|
scheme: https
|
||||||
|
authorization:
|
||||||
|
credentials: $ACCESS_TOKEN # this should have the bearer prefix removed
|
||||||
|
metrics_path: /api/v1/akkoma/metrics
|
||||||
|
static_configs:
|
||||||
|
- targets:
|
||||||
|
- example.com
|
||||||
|
```
|
|
@ -62,6 +62,7 @@ To add configuration to your config file, you can copy it from the base config.
|
||||||
* `password_reset_token_validity`: The time after which reset tokens aren't accepted anymore, in seconds (default: one day).
|
* `password_reset_token_validity`: The time after which reset tokens aren't accepted anymore, in seconds (default: one day).
|
||||||
* `local_bubble`: Array of domains representing instances closely related to yours. Used to populate the `bubble` timeline. e.g `["example.com"]`, (default: `[]`)
|
* `local_bubble`: Array of domains representing instances closely related to yours. Used to populate the `bubble` timeline. e.g `["example.com"]`, (default: `[]`)
|
||||||
* `languages`: List of Language Codes used by the instance. This is used to try and set a default language from the frontend. It will try and find the first match between the languages set here and the user's browser languages. It will default to the first language in this setting if there is no match.. (default `["en"]`)
|
* `languages`: List of Language Codes used by the instance. This is used to try and set a default language from the frontend. It will try and find the first match between the languages set here and the user's browser languages. It will default to the first language in this setting if there is no match.. (default `["en"]`)
|
||||||
|
* `export_prometheus_metrics`: Enable prometheus metrics, served at `/api/v1/akkoma/metrics`, requiring the `admin:metrics` oauth scope.
|
||||||
|
|
||||||
## :database
|
## :database
|
||||||
* `improved_hashtag_timeline`: Setting to force toggle / force disable improved hashtags timeline. `:enabled` forces hashtags to be fetched from `hashtags` table for hashtags timeline. `:disabled` forces object-embedded hashtags to be used (slower). Keep it `:auto` for automatic behaviour (it is auto-set to `:enabled` [unless overridden] when HashtagsTableMigrator completes).
|
* `improved_hashtag_timeline`: Setting to force toggle / force disable improved hashtags timeline. `:enabled` forces hashtags to be fetched from `hashtags` table for hashtags timeline. `:disabled` forces object-embedded hashtags to be used (slower). Keep it `:auto` for automatic behaviour (it is auto-set to `:enabled` [unless overridden] when HashtagsTableMigrator completes).
|
||||||
|
|
|
@ -73,7 +73,8 @@ def start(_type, _args) do
|
||||||
Pleroma.JobQueueMonitor,
|
Pleroma.JobQueueMonitor,
|
||||||
{Majic.Pool, [name: Pleroma.MajicPool, pool_size: Config.get([:majic_pool, :size], 2)]},
|
{Majic.Pool, [name: Pleroma.MajicPool, pool_size: Config.get([:majic_pool, :size], 2)]},
|
||||||
{Oban, Config.get(Oban)},
|
{Oban, Config.get(Oban)},
|
||||||
Pleroma.Web.Endpoint
|
Pleroma.Web.Endpoint,
|
||||||
|
Pleroma.Web.Telemetry
|
||||||
] ++
|
] ++
|
||||||
elasticsearch_children() ++
|
elasticsearch_children() ++
|
||||||
task_children(@mix_env) ++
|
task_children(@mix_env) ++
|
||||||
|
|
|
@ -65,7 +65,7 @@ def request(method, url, body, headers, options) when is_binary(url) do
|
||||||
options = put_in(options[:adapter], adapter_opts)
|
options = put_in(options[:adapter], adapter_opts)
|
||||||
params = options[:params] || []
|
params = options[:params] || []
|
||||||
request = build_request(method, headers, options, url, body, params)
|
request = build_request(method, headers, options, url, body, params)
|
||||||
client = Tesla.client([Tesla.Middleware.FollowRedirects])
|
client = Tesla.client([Tesla.Middleware.FollowRedirects, Tesla.Middleware.Telemetry])
|
||||||
|
|
||||||
request(client, request)
|
request(client, request)
|
||||||
end
|
end
|
||||||
|
|
24
lib/pleroma/web/akkoma_api/controllers/metrics_controller.ex
Normal file
24
lib/pleroma/web/akkoma_api/controllers/metrics_controller.ex
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
defmodule Pleroma.Web.AkkomaAPI.MetricsController do
|
||||||
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
|
alias Pleroma.Web.Plugs.OAuthScopesPlug
|
||||||
|
alias Pleroma.Config
|
||||||
|
|
||||||
|
plug(
|
||||||
|
OAuthScopesPlug,
|
||||||
|
%{scopes: ["admin:metrics"]}
|
||||||
|
when action in [
|
||||||
|
:show
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def show(conn, _params) do
|
||||||
|
if Config.get([:instance, :export_prometheus_metrics], true) do
|
||||||
|
conn
|
||||||
|
|> text(TelemetryMetricsPrometheus.Core.scrape())
|
||||||
|
else
|
||||||
|
conn
|
||||||
|
|> send_resp(404, "Not Found")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -13,6 +13,7 @@ defmodule Pleroma.Web.Endpoint do
|
||||||
|
|
||||||
plug(Pleroma.Web.Plugs.SetLocalePlug)
|
plug(Pleroma.Web.Plugs.SetLocalePlug)
|
||||||
plug(CORSPlug)
|
plug(CORSPlug)
|
||||||
|
plug(Pleroma.Web.Plugs.CSPNoncePlug)
|
||||||
plug(Pleroma.Web.Plugs.HTTPSecurityPlug)
|
plug(Pleroma.Web.Plugs.HTTPSecurityPlug)
|
||||||
plug(Pleroma.Web.Plugs.UploadedMedia)
|
plug(Pleroma.Web.Plugs.UploadedMedia)
|
||||||
|
|
||||||
|
|
|
@ -211,11 +211,11 @@ defp handle_create_authorization_error(
|
||||||
{:error, scopes_issue},
|
{:error, scopes_issue},
|
||||||
%{"authorization" => _} = params
|
%{"authorization" => _} = params
|
||||||
)
|
)
|
||||||
when scopes_issue in [:unsupported_scopes, :missing_scopes] do
|
when scopes_issue in [:unsupported_scopes, :missing_scopes, :user_is_not_an_admin] do
|
||||||
# Per https://github.com/tootsuite/mastodon/blob/
|
# Per https://github.com/tootsuite/mastodon/blob/
|
||||||
# 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39
|
# 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39
|
||||||
conn
|
conn
|
||||||
|> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes"))
|
|> put_flash(:error, dgettext("errors", "This action is outside of authorized scopes"))
|
||||||
|> put_status(:unauthorized)
|
|> put_status(:unauthorized)
|
||||||
|> authorize(params)
|
|> authorize(params)
|
||||||
end
|
end
|
||||||
|
@ -605,7 +605,7 @@ defp do_create_authorization(
|
||||||
defp do_create_authorization(%User{} = user, %App{} = app, requested_scopes)
|
defp do_create_authorization(%User{} = user, %App{} = app, requested_scopes)
|
||||||
when is_list(requested_scopes) do
|
when is_list(requested_scopes) do
|
||||||
with {:account_status, :active} <- {:account_status, User.account_status(user)},
|
with {:account_status, :active} <- {:account_status, User.account_status(user)},
|
||||||
{:ok, scopes} <- validate_scopes(app, requested_scopes),
|
{:ok, scopes} <- validate_scopes(user, app, requested_scopes),
|
||||||
{:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
|
{:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
|
||||||
{:ok, auth}
|
{:ok, auth}
|
||||||
end
|
end
|
||||||
|
@ -637,15 +637,16 @@ defp build_and_response_mfa_token(user, auth) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec validate_scopes(App.t(), map() | list()) ::
|
@spec validate_scopes(User.t(), App.t(), map() | list()) ::
|
||||||
{:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
|
{:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
|
||||||
defp validate_scopes(%App{} = app, params) when is_map(params) do
|
defp validate_scopes(%User{} = user, %App{} = app, params) when is_map(params) do
|
||||||
requested_scopes = Scopes.fetch_scopes(params, app.scopes)
|
requested_scopes = Scopes.fetch_scopes(params, app.scopes)
|
||||||
validate_scopes(app, requested_scopes)
|
validate_scopes(user, app, requested_scopes)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp validate_scopes(%App{} = app, requested_scopes) when is_list(requested_scopes) do
|
defp validate_scopes(%User{} = user, %App{} = app, requested_scopes)
|
||||||
Scopes.validate(requested_scopes, app.scopes)
|
when is_list(requested_scopes) do
|
||||||
|
Scopes.validate(requested_scopes, app.scopes, user)
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_redirect_uri(%App{} = app) do
|
def default_redirect_uri(%App{} = app) do
|
||||||
|
|
|
@ -56,12 +56,20 @@ def to_string(scopes), do: Enum.join(scopes, " ")
|
||||||
@doc """
|
@doc """
|
||||||
Validates scopes.
|
Validates scopes.
|
||||||
"""
|
"""
|
||||||
@spec validate(list() | nil, list()) ::
|
@spec validate(list() | nil, list(), Pleroma.User.t()) ::
|
||||||
{:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
|
{:ok, list()} | {:error, :missing_scopes | :unsupported_scopes, :user_is_not_an_admin}
|
||||||
def validate(blank_scopes, _app_scopes) when blank_scopes in [nil, []],
|
def validate(blank_scopes, _app_scopes, _user) when blank_scopes in [nil, []],
|
||||||
do: {:error, :missing_scopes}
|
do: {:error, :missing_scopes}
|
||||||
|
|
||||||
def validate(scopes, app_scopes) do
|
def validate(scopes, app_scopes, %Pleroma.User{is_admin: is_admin}) do
|
||||||
|
if !is_admin && contains_admin_scopes?(scopes) do
|
||||||
|
{:error, :user_is_not_an_admin}
|
||||||
|
else
|
||||||
|
validate_scopes_are_supported(scopes, app_scopes)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_scopes_are_supported(scopes, app_scopes) do
|
||||||
case OAuthScopesPlug.filter_descendants(scopes, app_scopes) do
|
case OAuthScopesPlug.filter_descendants(scopes, app_scopes) do
|
||||||
^scopes -> {:ok, scopes}
|
^scopes -> {:ok, scopes}
|
||||||
_ -> {:error, :unsupported_scopes}
|
_ -> {:error, :unsupported_scopes}
|
||||||
|
|
21
lib/pleroma/web/plugs/csp_nonce_plug.ex
Normal file
21
lib/pleroma/web/plugs/csp_nonce_plug.ex
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
defmodule Pleroma.Web.Plugs.CSPNoncePlug do
|
||||||
|
import Plug.Conn
|
||||||
|
|
||||||
|
def init(opts) do
|
||||||
|
opts
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(conn, _opts) do
|
||||||
|
assign_csp_nonce(conn)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp assign_csp_nonce(conn) do
|
||||||
|
nonce =
|
||||||
|
:crypto.strong_rand_bytes(128)
|
||||||
|
|> Base.url_encode64()
|
||||||
|
|> binary_part(0, 15)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> assign(:csp_nonce, nonce)
|
||||||
|
end
|
||||||
|
end
|
|
@ -13,7 +13,7 @@ def init(opts), do: opts
|
||||||
def call(conn, _options) do
|
def call(conn, _options) do
|
||||||
if Config.get([:http_security, :enabled]) do
|
if Config.get([:http_security, :enabled]) do
|
||||||
conn
|
conn
|
||||||
|> merge_resp_headers(headers())
|
|> merge_resp_headers(headers(conn))
|
||||||
|> maybe_send_sts_header(Config.get([:http_security, :sts]))
|
|> maybe_send_sts_header(Config.get([:http_security, :sts]))
|
||||||
else
|
else
|
||||||
conn
|
conn
|
||||||
|
@ -36,7 +36,8 @@ def custom_http_frontend_headers do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def headers do
|
@spec headers(Plug.Conn.t()) :: [{String.t(), String.t()}]
|
||||||
|
def headers(conn) do
|
||||||
referrer_policy = Config.get([:http_security, :referrer_policy])
|
referrer_policy = Config.get([:http_security, :referrer_policy])
|
||||||
report_uri = Config.get([:http_security, :report_uri])
|
report_uri = Config.get([:http_security, :report_uri])
|
||||||
custom_http_frontend_headers = custom_http_frontend_headers()
|
custom_http_frontend_headers = custom_http_frontend_headers()
|
||||||
|
@ -47,7 +48,7 @@ def headers do
|
||||||
{"x-frame-options", "DENY"},
|
{"x-frame-options", "DENY"},
|
||||||
{"x-content-type-options", "nosniff"},
|
{"x-content-type-options", "nosniff"},
|
||||||
{"referrer-policy", referrer_policy},
|
{"referrer-policy", referrer_policy},
|
||||||
{"content-security-policy", csp_string()},
|
{"content-security-policy", csp_string(conn)},
|
||||||
{"permissions-policy", "interest-cohort=()"}
|
{"permissions-policy", "interest-cohort=()"}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -77,19 +78,18 @@ def headers do
|
||||||
"default-src 'none'",
|
"default-src 'none'",
|
||||||
"base-uri 'none'",
|
"base-uri 'none'",
|
||||||
"frame-ancestors 'none'",
|
"frame-ancestors 'none'",
|
||||||
"style-src 'self' 'unsafe-inline'",
|
|
||||||
"font-src 'self'",
|
|
||||||
"manifest-src 'self'"
|
"manifest-src 'self'"
|
||||||
]
|
]
|
||||||
|
|
||||||
@csp_start [Enum.join(static_csp_rules, ";") <> ";"]
|
@csp_start [Enum.join(static_csp_rules, ";") <> ";"]
|
||||||
|
|
||||||
defp csp_string do
|
defp csp_string(conn) do
|
||||||
scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme]
|
scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme]
|
||||||
static_url = Pleroma.Web.Endpoint.static_url()
|
static_url = Pleroma.Web.Endpoint.static_url()
|
||||||
websocket_url = Pleroma.Web.Endpoint.websocket_url()
|
websocket_url = Pleroma.Web.Endpoint.websocket_url()
|
||||||
report_uri = Config.get([:http_security, :report_uri])
|
report_uri = Config.get([:http_security, :report_uri])
|
||||||
|
%{assigns: %{csp_nonce: nonce}} = conn
|
||||||
|
nonce_tag = "nonce-" <> nonce
|
||||||
img_src = "img-src 'self' data: blob:"
|
img_src = "img-src 'self' data: blob:"
|
||||||
media_src = "media-src 'self'"
|
media_src = "media-src 'self'"
|
||||||
|
|
||||||
|
@ -111,11 +111,14 @@ defp csp_string do
|
||||||
["connect-src 'self' blob: ", static_url, ?\s, websocket_url]
|
["connect-src 'self' blob: ", static_url, ?\s, websocket_url]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
style_src = "style-src 'self' 'unsafe-inline'"
|
||||||
|
font_src = "font-src 'self' data:"
|
||||||
|
|
||||||
script_src =
|
script_src =
|
||||||
if Config.get(:env) == :dev do
|
if Config.get(:env) == :dev do
|
||||||
"script-src 'self' 'unsafe-eval'"
|
"script-src 'self' 'unsafe-eval' '#{nonce_tag}'"
|
||||||
else
|
else
|
||||||
"script-src 'self'"
|
"script-src 'self' '#{nonce_tag}'"
|
||||||
end
|
end
|
||||||
|
|
||||||
report = if report_uri, do: ["report-uri ", report_uri, ";report-to csp-endpoint"]
|
report = if report_uri, do: ["report-uri ", report_uri, ";report-to csp-endpoint"]
|
||||||
|
@ -126,6 +129,8 @@ defp csp_string do
|
||||||
|> add_csp_param(media_src)
|
|> add_csp_param(media_src)
|
||||||
|> add_csp_param(connect_src)
|
|> add_csp_param(connect_src)
|
||||||
|> add_csp_param(script_src)
|
|> add_csp_param(script_src)
|
||||||
|
|> add_csp_param(font_src)
|
||||||
|
|> add_csp_param(style_src)
|
||||||
|> add_csp_param(insecure)
|
|> add_csp_param(insecure)
|
||||||
|> add_csp_param(report)
|
|> add_csp_param(report)
|
||||||
|> :erlang.iolist_to_binary()
|
|> :erlang.iolist_to_binary()
|
||||||
|
|
|
@ -197,12 +197,18 @@ defp incorporate_conn_info(action_settings, %{params: params} = conn) do
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
defp ip(%{remote_ip: remote_ip}) do
|
defp ip(%{remote_ip: remote_ip}) when is_binary(remote_ip) do
|
||||||
|
remote_ip
|
||||||
|
end
|
||||||
|
|
||||||
|
defp ip(%{remote_ip: remote_ip}) when is_tuple(remote_ip) do
|
||||||
remote_ip
|
remote_ip
|
||||||
|> Tuple.to_list()
|
|> Tuple.to_list()
|
||||||
|> Enum.join(".")
|
|> Enum.join(".")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp ip(_), do: nil
|
||||||
|
|
||||||
defp render_throttled_error(conn) do
|
defp render_throttled_error(conn) do
|
||||||
conn
|
conn
|
||||||
|> render_error(:too_many_requests, "Throttled")
|
|> render_error(:too_many_requests, "Throttled")
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
defmodule Pleroma.Web.Preload do
|
defmodule Pleroma.Web.Preload do
|
||||||
alias Phoenix.HTML
|
alias Phoenix.HTML
|
||||||
|
|
||||||
def build_tags(_conn, params) do
|
def build_tags(%{assigns: %{csp_nonce: nonce}} = conn, params) do
|
||||||
preload_data =
|
preload_data =
|
||||||
Enum.reduce(Pleroma.Config.get([__MODULE__, :providers], []), %{}, fn parser, acc ->
|
Enum.reduce(Pleroma.Config.get([__MODULE__, :providers], []), %{}, fn parser, acc ->
|
||||||
terms =
|
terms =
|
||||||
|
@ -20,16 +20,17 @@ def build_tags(_conn, params) do
|
||||||
rendered_html =
|
rendered_html =
|
||||||
preload_data
|
preload_data
|
||||||
|> Jason.encode!()
|
|> Jason.encode!()
|
||||||
|> build_script_tag()
|
|> build_script_tag(nonce)
|
||||||
|> HTML.safe_to_string()
|
|> HTML.safe_to_string()
|
||||||
|
|
||||||
rendered_html
|
rendered_html
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_script_tag(content) do
|
def build_script_tag(content, nonce) do
|
||||||
HTML.Tag.content_tag(:script, HTML.raw(content),
|
HTML.Tag.content_tag(:script, HTML.raw(content),
|
||||||
id: "initial-results",
|
id: "initial-results",
|
||||||
type: "application/json"
|
type: "application/json",
|
||||||
|
nonce: nonce
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -467,6 +467,7 @@ defmodule Pleroma.Web.Router do
|
||||||
|
|
||||||
scope "/api/v1/akkoma", Pleroma.Web.AkkomaAPI do
|
scope "/api/v1/akkoma", Pleroma.Web.AkkomaAPI do
|
||||||
pipe_through(:authenticated_api)
|
pipe_through(:authenticated_api)
|
||||||
|
get("/metrics", MetricsController, :show)
|
||||||
get("/translation/languages", TranslationController, :languages)
|
get("/translation/languages", TranslationController, :languages)
|
||||||
|
|
||||||
get("/frontend_settings/:frontend_name", FrontendSettingsController, :list_profiles)
|
get("/frontend_settings/:frontend_name", FrontendSettingsController, :list_profiles)
|
||||||
|
@ -867,7 +868,11 @@ defmodule Pleroma.Web.Router do
|
||||||
|
|
||||||
scope "/" do
|
scope "/" do
|
||||||
pipe_through([:pleroma_html, :authenticate, :require_admin])
|
pipe_through([:pleroma_html, :authenticate, :require_admin])
|
||||||
live_dashboard("/phoenix/live_dashboard")
|
|
||||||
|
live_dashboard("/phoenix/live_dashboard",
|
||||||
|
metrics: {Pleroma.Web.Telemetry, :live_dashboard_metrics},
|
||||||
|
csp_nonce_assign_key: :csp_nonce
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Test-only routes needed to test action dispatching and plug chain execution
|
# Test-only routes needed to test action dispatching and plug chain execution
|
||||||
|
@ -906,6 +911,7 @@ defmodule Pleroma.Web.Router do
|
||||||
scope "/", Pleroma.Web.Fallback do
|
scope "/", Pleroma.Web.Fallback do
|
||||||
get("/registration/:token", RedirectController, :registration_page)
|
get("/registration/:token", RedirectController, :registration_page)
|
||||||
get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta)
|
get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta)
|
||||||
|
get("/api/*path", RedirectController, :api_not_implemented)
|
||||||
get("/*path", RedirectController, :redirector_with_preload)
|
get("/*path", RedirectController, :redirector_with_preload)
|
||||||
|
|
||||||
options("/*path", RedirectController, :empty)
|
options("/*path", RedirectController, :empty)
|
||||||
|
|
131
lib/pleroma/web/telemetry.ex
Normal file
131
lib/pleroma/web/telemetry.ex
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
defmodule Pleroma.Web.Telemetry do
|
||||||
|
use Supervisor
|
||||||
|
import Telemetry.Metrics
|
||||||
|
alias Pleroma.Stats
|
||||||
|
|
||||||
|
def start_link(arg) do
|
||||||
|
Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init(_arg) do
|
||||||
|
children = [
|
||||||
|
{:telemetry_poller, measurements: periodic_measurements(), period: 10_000},
|
||||||
|
{TelemetryMetricsPrometheus.Core, metrics: prometheus_metrics()}
|
||||||
|
]
|
||||||
|
|
||||||
|
Supervisor.init(children, strategy: :one_for_one)
|
||||||
|
end
|
||||||
|
|
||||||
|
# A seperate set of metrics for distributions because phoenix dashboard does NOT handle them well
|
||||||
|
defp distribution_metrics do
|
||||||
|
[
|
||||||
|
distribution(
|
||||||
|
"phoenix.router_dispatch.stop.duration",
|
||||||
|
# event_name: [:pleroma, :repo, :query, :total_time],
|
||||||
|
measurement: :duration,
|
||||||
|
unit: {:native, :second},
|
||||||
|
tags: [:route],
|
||||||
|
reporter_options: [
|
||||||
|
buckets: [0.1, 0.2, 0.5, 1, 2.5, 5, 10, 25, 50, 100, 250, 500, 1000]
|
||||||
|
]
|
||||||
|
),
|
||||||
|
|
||||||
|
# Database Time Metrics
|
||||||
|
distribution(
|
||||||
|
"pleroma.repo.query.total_time",
|
||||||
|
# event_name: [:pleroma, :repo, :query, :total_time],
|
||||||
|
measurement: :total_time,
|
||||||
|
unit: {:native, :millisecond},
|
||||||
|
reporter_options: [
|
||||||
|
buckets: [0.1, 0.2, 0.5, 1, 2.5, 5, 10, 25, 50, 100, 250, 500, 1000]
|
||||||
|
]
|
||||||
|
),
|
||||||
|
distribution(
|
||||||
|
"pleroma.repo.query.queue_time",
|
||||||
|
# event_name: [:pleroma, :repo, :query, :total_time],
|
||||||
|
measurement: :queue_time,
|
||||||
|
unit: {:native, :millisecond},
|
||||||
|
reporter_options: [
|
||||||
|
buckets: [0.01, 0.025, 0.05, 0.1, 0.2, 0.5, 1, 2.5, 5, 10]
|
||||||
|
]
|
||||||
|
),
|
||||||
|
distribution(
|
||||||
|
"oban_job_exception",
|
||||||
|
event_name: [:oban, :job, :exception],
|
||||||
|
measurement: :duration,
|
||||||
|
tags: [:worker],
|
||||||
|
tag_values: fn tags -> Map.put(tags, :worker, tags.job.worker) end,
|
||||||
|
unit: {:native, :second},
|
||||||
|
reporter_options: [
|
||||||
|
buckets: [0.01, 0.025, 0.05, 0.1, 0.2, 0.5, 1, 2.5, 5, 10]
|
||||||
|
]
|
||||||
|
),
|
||||||
|
distribution(
|
||||||
|
"tesla_request_completed",
|
||||||
|
event_name: [:tesla, :request, :stop],
|
||||||
|
measurement: :duration,
|
||||||
|
tags: [:response_code],
|
||||||
|
tag_values: fn tags -> Map.put(tags, :response_code, tags.env.status) end,
|
||||||
|
unit: {:native, :second},
|
||||||
|
reporter_options: [
|
||||||
|
buckets: [0.01, 0.025, 0.05, 0.1, 0.2, 0.5, 1, 2.5, 5, 10]
|
||||||
|
]
|
||||||
|
),
|
||||||
|
distribution(
|
||||||
|
"oban_job_completion",
|
||||||
|
event_name: [:oban, :job, :stop],
|
||||||
|
measurement: :duration,
|
||||||
|
tags: [:worker],
|
||||||
|
tag_values: fn tags -> Map.put(tags, :worker, tags.job.worker) end,
|
||||||
|
unit: {:native, :second},
|
||||||
|
reporter_options: [
|
||||||
|
buckets: [0.01, 0.025, 0.05, 0.1, 0.2, 0.5, 1, 2.5, 5, 10]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp summary_metrics do
|
||||||
|
[
|
||||||
|
# Phoenix Metrics
|
||||||
|
summary("phoenix.endpoint.stop.duration",
|
||||||
|
unit: {:native, :millisecond}
|
||||||
|
),
|
||||||
|
summary("phoenix.router_dispatch.stop.duration",
|
||||||
|
tags: [:route],
|
||||||
|
unit: {:native, :millisecond}
|
||||||
|
),
|
||||||
|
summary("pleroma.repo.query.total_time", unit: {:native, :millisecond}),
|
||||||
|
summary("pleroma.repo.query.decode_time", unit: {:native, :millisecond}),
|
||||||
|
summary("pleroma.repo.query.query_time", unit: {:native, :millisecond}),
|
||||||
|
summary("pleroma.repo.query.queue_time", unit: {:native, :millisecond}),
|
||||||
|
summary("pleroma.repo.query.idle_time", unit: {:native, :millisecond}),
|
||||||
|
|
||||||
|
# VM Metrics
|
||||||
|
summary("vm.memory.total", unit: {:byte, :kilobyte}),
|
||||||
|
summary("vm.total_run_queue_lengths.total"),
|
||||||
|
summary("vm.total_run_queue_lengths.cpu"),
|
||||||
|
summary("vm.total_run_queue_lengths.io"),
|
||||||
|
last_value("pleroma.local_users.total"),
|
||||||
|
last_value("pleroma.domains.total"),
|
||||||
|
last_value("pleroma.local_statuses.total")
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def prometheus_metrics, do: summary_metrics() ++ distribution_metrics()
|
||||||
|
def live_dashboard_metrics, do: summary_metrics()
|
||||||
|
|
||||||
|
defp periodic_measurements do
|
||||||
|
[
|
||||||
|
{__MODULE__, :instance_stats, []}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def instance_stats do
|
||||||
|
stats = Stats.get_stats()
|
||||||
|
:telemetry.execute([:pleroma, :local_users], %{total: stats.user_count}, %{})
|
||||||
|
:telemetry.execute([:pleroma, :domains], %{total: stats.domain_count}, %{})
|
||||||
|
:telemetry.execute([:pleroma, :local_statuses], %{total: stats.status_count}, %{})
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,17 +4,33 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui">
|
<meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui">
|
||||||
<title><%= Pleroma.Config.get([:instance, :name]) %></title>
|
<title><%= Pleroma.Config.get([:instance, :name]) %></title>
|
||||||
<link rel="stylesheet" href="/instance/static.css">
|
<link rel="stylesheet" href="/static-fe/static-fe.css">
|
||||||
|
<link rel="stylesheet" href="/static-fe/forms.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="instance-header">
|
|
||||||
<a class="instance-header__content" href="/">
|
<div class="background-image"></div>
|
||||||
<img class="instance-header__thumbnail" src="<%= Pleroma.Config.get([:instance, :instance_thumbnail]) %>">
|
<nav>
|
||||||
<h1 class="instance-header__title"><%= Pleroma.Config.get([:instance, :name]) %></h1>
|
<div class="inner-nav">
|
||||||
|
<a class="site-brand" href="/">
|
||||||
|
<img class="favicon" src="/favicon.png" />
|
||||||
|
<span><%= Pleroma.Config.get([:instance, :name]) %></span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
</nav>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<div class="underlay"></div>
|
||||||
|
<div class="column main flex">
|
||||||
|
<div class="panel oauth">
|
||||||
<%= @inner_content %>
|
<%= @inner_content %>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--background-image: url("<%= Pleroma.Config.get([:instance, :background_image]) %>");
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,24 +1,29 @@
|
||||||
<%= if get_flash(@conn, :info) do %>
|
<div>
|
||||||
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
|
<%= if get_flash(@conn, :info) do %>
|
||||||
<% end %>
|
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
|
||||||
<%= if get_flash(@conn, :error) do %>
|
<% end %>
|
||||||
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
|
<%= if get_flash(@conn, :error) do %>
|
||||||
<% end %>
|
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
|
||||||
|
<% end %>
|
||||||
<h2><%= Gettext.dpgettext("static_pages", "mfa recover page title", "Two-factor recovery") %></h2>
|
<div class="panel-heading">
|
||||||
|
<%= Gettext.dpgettext("static_pages", "mfa recover page title", "Two-factor recovery") %>
|
||||||
<%= form_for @conn, Routes.mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
|
</div>
|
||||||
<div class="input">
|
<div class="panel-content">
|
||||||
|
<%= form_for @conn, Routes.mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
|
||||||
|
<div class="input">
|
||||||
<%= label f, :code, Gettext.dpgettext("static_pages", "mfa recover recovery code prompt", "Recovery code") %>
|
<%= label f, :code, Gettext.dpgettext("static_pages", "mfa recover recovery code prompt", "Recovery code") %>
|
||||||
<%= text_input f, :code, [autocomplete: false, autocorrect: "off", autocapitalize: "off", autofocus: true, spellcheck: false] %>
|
<%= text_input f, :code, [autocomplete: false, autocorrect: "off", autocapitalize: "off", autofocus: true, spellcheck: false] %>
|
||||||
<%= hidden_input f, :mfa_token, value: @mfa_token %>
|
<%= hidden_input f, :mfa_token, value: @mfa_token %>
|
||||||
<%= hidden_input f, :state, value: @state %>
|
<%= hidden_input f, :state, value: @state %>
|
||||||
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
|
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
|
||||||
<%= hidden_input f, :challenge_type, value: "recovery" %>
|
<%= hidden_input f, :challenge_type, value: "recovery" %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= submit Gettext.dpgettext("static_pages", "mfa recover verify recovery code button", "Verify") %>
|
<%= submit Gettext.dpgettext("static_pages", "mfa recover verify recovery code button", "Verify") %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<a href="<%= Routes.mfa_path(@conn, :show, %{challenge_type: "totp", mfa_token: @mfa_token, state: @state, redirect_uri: @redirect_uri}) %>">
|
<a href="<%= Routes.mfa_path(@conn, :show, %{challenge_type: "totp", mfa_token: @mfa_token, state: @state, redirect_uri: @redirect_uri}) %>">
|
||||||
<%= Gettext.dpgettext("static_pages", "mfa recover use 2fa code link", "Enter a two-factor code") %>
|
<%= Gettext.dpgettext("static_pages", "mfa recover use 2fa code link", "Enter a two-factor code") %>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
|
@ -1,24 +1,28 @@
|
||||||
<%= if get_flash(@conn, :info) do %>
|
<div>
|
||||||
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
|
<%= if get_flash(@conn, :info) do %>
|
||||||
<% end %>
|
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
|
||||||
<%= if get_flash(@conn, :error) do %>
|
<% end %>
|
||||||
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
|
<%= if get_flash(@conn, :error) do %>
|
||||||
<% end %>
|
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
|
||||||
|
<% end %>
|
||||||
<h2><%= Gettext.dpgettext("static_pages", "mfa auth page title", "Two-factor authentication") %></h2>
|
<div class="panel-heading">
|
||||||
|
<%= Gettext.dpgettext("static_pages", "mfa auth page title", "Two-factor authentication") %>
|
||||||
<%= form_for @conn, Routes.mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
|
</div>
|
||||||
<div class="input">
|
<div class="panel-content">
|
||||||
|
<%= form_for @conn, Routes.mfa_verify_path(@conn, :verify), [as: "mfa"], fn f -> %>
|
||||||
|
<div class="input">
|
||||||
<%= label f, :code, Gettext.dpgettext("static_pages", "mfa auth code prompt", "Authentication code") %>
|
<%= label f, :code, Gettext.dpgettext("static_pages", "mfa auth code prompt", "Authentication code") %>
|
||||||
<%= text_input f, :code, [autocomplete: "one-time-code", autocorrect: "off", autocapitalize: "off", autofocus: true, pattern: "[0-9]*", spellcheck: false] %>
|
<%= text_input f, :code, [autocomplete: "one-time-code", autocorrect: "off", autocapitalize: "off", autofocus: true, pattern: "[0-9]*", spellcheck: false] %>
|
||||||
<%= hidden_input f, :mfa_token, value: @mfa_token %>
|
<%= hidden_input f, :mfa_token, value: @mfa_token %>
|
||||||
<%= hidden_input f, :state, value: @state %>
|
<%= hidden_input f, :state, value: @state %>
|
||||||
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
|
<%= hidden_input f, :redirect_uri, value: @redirect_uri %>
|
||||||
<%= hidden_input f, :challenge_type, value: "totp" %>
|
<%= hidden_input f, :challenge_type, value: "totp" %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= submit Gettext.dpgettext("static_pages", "mfa auth verify code button", "Verify") %>
|
<%= submit Gettext.dpgettext("static_pages", "mfa auth verify code button", "Verify") %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<a href="<%= Routes.mfa_path(@conn, :show, %{challenge_type: "recovery", mfa_token: @mfa_token, state: @state, redirect_uri: @redirect_uri}) %>">
|
<a href="<%= Routes.mfa_path(@conn, :show, %{challenge_type: "recovery", mfa_token: @mfa_token, state: @state, redirect_uri: @redirect_uri}) %>">
|
||||||
<%= Gettext.dpgettext("static_pages", "mfa auth page use recovery code link", "Enter a two-factor recovery code") %>
|
<%= Gettext.dpgettext("static_pages", "mfa auth page use recovery code link", "Enter a two-factor recovery code") %>
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
|
@ -1,2 +1,8 @@
|
||||||
<h1><%= Gettext.dpgettext("static_pages", "oauth authorized page title", "Successfully authorized") %></h1>
|
<div>
|
||||||
<h2><%= raw Gettext.dpgettext("static_pages", "oauth token code message", "Token code is <br>%{token}", token: safe_to_string(html_escape(@auth.token))) %></h2>
|
<div class="panel-heading">
|
||||||
|
<%= Gettext.dpgettext("static_pages", "oauth authorized page title", "Successfully authorized") %>
|
||||||
|
</div>
|
||||||
|
<div class="panel-content">
|
||||||
|
<%= raw Gettext.dpgettext("static_pages", "oauth token code message", "Token code is <br>%{token}", token: safe_to_string(html_escape(@auth.token))) %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
|
@ -1,2 +1,8 @@
|
||||||
<h1><%= Gettext.dpgettext("static_pages", "oauth authorization exists page title", "Authorization exists") %></h1>
|
<div>
|
||||||
<h2><%= raw Gettext.dpgettext("static_pages", "oauth token code message", "Token code is <br>%{token}", token: safe_to_string(html_escape(@token.token))) %></h2>
|
<div class="panel-heading">
|
||||||
|
<%= Gettext.dpgettext("static_pages", "oauth authorization exists page title", "Authorization exists") %>
|
||||||
|
</div>
|
||||||
|
<div class="panel-content">
|
||||||
|
<%= raw Gettext.dpgettext("static_pages", "oauth token code message", "Token code is <br>%{token}", token: safe_to_string(html_escape(@token.token))) %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
|
@ -10,30 +10,35 @@
|
||||||
<%= if @user do %>
|
<%= if @user do %>
|
||||||
<div class="account-header">
|
<div class="account-header">
|
||||||
<div class="account-header__banner" style="background-image: url('<%= Pleroma.User.banner_url(@user) %>')"></div>
|
<div class="account-header__banner" style="background-image: url('<%= Pleroma.User.banner_url(@user) %>')"></div>
|
||||||
<div class="account-header__avatar" style="background-image: url('<%= Pleroma.User.avatar_url(@user) %>')"></div>
|
<div class="account-header__avatar" style="background-image: url('<%= Pleroma.User.avatar_url(@user) %>')">
|
||||||
<div class="account-header__meta">
|
<div class="account-header__meta">
|
||||||
<div class="account-header__display-name"><%= @user.name %></div>
|
<div class="account-header__display-name"><%= @user.name %></div>
|
||||||
<div class="account-header__nickname">@<%= @user.nickname %>@<%= Pleroma.User.get_host(@user) %></div>
|
<div class="account-header__nickname">@<%= @user.nickname %>@<%= Pleroma.User.get_host(@user) %></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="container__content">
|
<div class="container__content">
|
||||||
<%= if @app do %>
|
<%= if @app do %>
|
||||||
|
<div class="panel-heading">
|
||||||
<p><%= raw Gettext.dpgettext("static_pages", "oauth authorize message", "Application <strong>%{client_name}</strong> is requesting access to your account.", client_name: safe_to_string(html_escape(@app.client_name))) %></p>
|
<p><%= raw Gettext.dpgettext("static_pages", "oauth authorize message", "Application <strong>%{client_name}</strong> is requesting access to your account.", client_name: safe_to_string(html_escape(@app.client_name))) %></p>
|
||||||
<%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<div class="panel-content">
|
||||||
|
<%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %>
|
||||||
<%= if @user do %>
|
<%= if @user do %>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<a class="button button--cancel" href="/">
|
<a class="button button-cancel" href="/">
|
||||||
<%= Gettext.dpgettext("static_pages", "oauth authorize cancel button", "Cancel") %>
|
<%= Gettext.dpgettext("static_pages", "oauth authorize cancel button", "Cancel") %>
|
||||||
</a>
|
</a>
|
||||||
<%= submit Gettext.dpgettext("static_pages", "oauth authorize approve button", "Approve"), class: "button--approve" %>
|
<%= submit Gettext.dpgettext("static_pages", "oauth authorize approve button", "Approve"), class: "button--approve" %>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= if @params["registration"] in ["true", true] do %>
|
<%= if @params["registration"] in ["true", true] do %>
|
||||||
<h3><%= Gettext.dpgettext("static_pages", "oauth register page title", "This is the first time you visit! Please enter your Pleroma handle.") %></h3>
|
<h3><%= Gettext.dpgettext("static_pages", "oauth register page title", "This is your first visit! Please enter your Akkoma handle.") %></h3>
|
||||||
<p><%= Gettext.dpgettext("static_pages", "oauth register nickname unchangeable warning", "Choose carefully! You won't be able to change this later. You will be able to change your display name, though.") %></p>
|
<p><%= Gettext.dpgettext("static_pages", "oauth register nickname unchangeable warning", "Choose carefully! You won't be able to change this later. You will be able to change your display name, though.") %></p>
|
||||||
<div class="input">
|
<div class="input">
|
||||||
<%= label f, :nickname, Gettext.dpgettext("static_pages", "oauth register nickname prompt", "Pleroma Handle") %>
|
<%= label f, :nickname, Gettext.dpgettext("static_pages", "oauth register nickname prompt", "Pleroma Handle") %>
|
||||||
|
@ -54,6 +59,7 @@
|
||||||
<%= submit Gettext.dpgettext("static_pages", "oauth login button", "Log In") %>
|
<%= submit Gettext.dpgettext("static_pages", "oauth login button", "Log In") %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= hidden_input f, :client_id, value: @client_id %>
|
<%= hidden_input f, :client_id, value: @client_id %>
|
||||||
|
|
3
mix.exs
3
mix.exs
|
@ -161,6 +161,9 @@ defp deps do
|
||||||
git: "https://akkoma.dev/AkkomaGang/linkify.git", branch: "bugfix/line-ending-buffer"},
|
git: "https://akkoma.dev/AkkomaGang/linkify.git", branch: "bugfix/line-ending-buffer"},
|
||||||
{:http_signatures, "~> 0.1.1"},
|
{:http_signatures, "~> 0.1.1"},
|
||||||
{:telemetry, "~> 0.3"},
|
{:telemetry, "~> 0.3"},
|
||||||
|
{:telemetry_poller, "~> 0.4"},
|
||||||
|
{:telemetry_metrics, "~> 0.4"},
|
||||||
|
{:telemetry_metrics_prometheus_core, "~> 1.1.0"},
|
||||||
{:poolboy, "~> 1.5"},
|
{:poolboy, "~> 1.5"},
|
||||||
{:recon, "~> 2.5"},
|
{:recon, "~> 2.5"},
|
||||||
{:joken, "~> 2.0"},
|
{:joken, "~> 2.0"},
|
||||||
|
|
3
mix.lock
3
mix.lock
|
@ -112,6 +112,9 @@
|
||||||
"table_rex": {:hex, :table_rex, "3.1.1", "0c67164d1714b5e806d5067c1e96ff098ba7ae79413cc075973e17c38a587caa", [:mix], [], "hexpm", "678a23aba4d670419c23c17790f9dcd635a4a89022040df7d5d772cb21012490"},
|
"table_rex": {:hex, :table_rex, "3.1.1", "0c67164d1714b5e806d5067c1e96ff098ba7ae79413cc075973e17c38a587caa", [:mix], [], "hexpm", "678a23aba4d670419c23c17790f9dcd635a4a89022040df7d5d772cb21012490"},
|
||||||
"telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"},
|
"telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"},
|
||||||
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
|
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
|
||||||
|
"telemetry_metrics_prometheus": {:hex, :telemetry_metrics_prometheus, "1.1.0", "1cc23e932c1ef9aa3b91db257ead31ea58d53229d407e059b29bb962c1505a13", [:mix], [{:plug_cowboy, "~> 2.1", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}], "hexpm", "d43b3659b3244da44fe0275b717701542365d4519b79d9ce895b9719c1ce4d26"},
|
||||||
|
"telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.1.0", "4e15f6d7dbedb3a4e3aed2262b7e1407f166fcb9c30ca3f96635dfbbef99965c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0dd10e7fe8070095df063798f82709b0a1224c31b8baf6278b423898d591a069"},
|
||||||
|
"telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"},
|
||||||
"temple": {:git, "https://akkoma.dev/AkkomaGang/temple.git", "066a699ade472d8fa42a9d730b29a61af9bc8b59", [ref: "066a699ade472d8fa42a9d730b29a61af9bc8b59"]},
|
"temple": {:git, "https://akkoma.dev/AkkomaGang/temple.git", "066a699ade472d8fa42a9d730b29a61af9bc8b59", [ref: "066a699ade472d8fa42a9d730b29a61af9bc8b59"]},
|
||||||
"tesla": {:hex, :tesla, "1.4.4", "bb89aa0c9745190930366f6a2ac612cdf2d0e4d7fff449861baa7875afd797b2", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.3", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "d5503a49f9dec1b287567ea8712d085947e247cb11b06bc54adb05bfde466457"},
|
"tesla": {:hex, :tesla, "1.4.4", "bb89aa0c9745190930366f6a2ac612cdf2d0e4d7fff449861baa7875afd797b2", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.3", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "d5503a49f9dec1b287567ea8712d085947e247cb11b06bc54adb05bfde466457"},
|
||||||
"timex": {:hex, :timex, "3.7.9", "790cdfc4acfce434e442f98c02ea6d84d0239073bfd668968f82ac63e9a6788d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "64691582e5bb87130f721fc709acfb70f24405833998fabf35be968984860ce1"},
|
"timex": {:hex, :timex, "3.7.9", "790cdfc4acfce434e442f98c02ea6d84d0239073bfd668968f82ac63e9a6788d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "64691582e5bb87130f721fc709acfb70f24405833998fabf35be968984860ce1"},
|
||||||
|
|
158
priv/static/static-fe/forms.css
Normal file
158
priv/static/static-fe/forms.css
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
form {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
color: var(--muted-text-color);
|
||||||
|
display: flex;
|
||||||
|
margin-left: 1em;
|
||||||
|
margin-right: 1em;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
padding: 10px;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
border: 0;
|
||||||
|
transition-property: border-bottom;
|
||||||
|
transition-duration: 0.35s;
|
||||||
|
border-bottom: 2px solid #2a384a;
|
||||||
|
font-size: 14px;
|
||||||
|
width: inherit;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scopes-input {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 1em 0;
|
||||||
|
color: var(--muted-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scopes-input label:first-child {
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scopes {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scope {
|
||||||
|
display: flex;
|
||||||
|
flex-basis: 100%;
|
||||||
|
height: 2em;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scope:before {
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
content: "✔\fe0e";
|
||||||
|
margin-left: 1em;
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="checkbox"]+label {
|
||||||
|
display: none;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="checkbox"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="checkbox"]+label:before {
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
color: white;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
border: 4px solid var(--background-color);
|
||||||
|
box-shadow: 0px 0px 1px 0 var(--brand-color);
|
||||||
|
width: 1.2em;
|
||||||
|
height: 1.2em;
|
||||||
|
margin-right: 1.0em;
|
||||||
|
content: "";
|
||||||
|
transition-property: background-color;
|
||||||
|
transition-duration: 0.35s;
|
||||||
|
color: var(--background-color);
|
||||||
|
margin-bottom: -0.2em;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="checkbox"]:checked+label:before {
|
||||||
|
background-color: var(--brand-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
a.button,
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #1c2a3a;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 16px;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 16px;
|
||||||
|
box-shadow: 0px 0px 2px 0px black,
|
||||||
|
0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset,
|
||||||
|
0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.button:hover,
|
||||||
|
button:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0px 0px 0px 1px var(--brand-color),
|
||||||
|
0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset,
|
||||||
|
0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions button,
|
||||||
|
.actions a.button {
|
||||||
|
width: auto;
|
||||||
|
margin-left: 2%;
|
||||||
|
width: 45%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-header__banner {
|
||||||
|
width: 100%;
|
||||||
|
height: 80px;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-header__avatar {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
margin: -60px 10px 10px;
|
||||||
|
border: 6px solid var(--foreground-color);
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-header__meta {
|
||||||
|
padding: 12px 20px 17px 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-header__display-name {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-header__nickname {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--muted-text-color);
|
||||||
|
}
|
|
@ -11,14 +11,14 @@ :root {
|
||||||
--profileTint: rgba(15, 22, 30, 0.5);
|
--profileTint: rgba(15, 22, 30, 0.5);
|
||||||
--btnText: rgba(185, 185, 186, 1);
|
--btnText: rgba(185, 185, 186, 1);
|
||||||
--btn: rgba(21, 30, 43, 1);
|
--btn: rgba(21, 30, 43, 1);
|
||||||
--btnShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1) , 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
--btnShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
||||||
--btnHoverShadow: 0px 0px 1px 2px rgba(185, 185, 186, 0.4) inset, 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
--btnHoverShadow: 0px 0px 1px 2px rgba(185, 185, 186, 0.4) inset, 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
||||||
--lightText: rgba(236, 236, 236, 1);
|
--lightText: rgba(236, 236, 236, 1);
|
||||||
--panelShadow: 0px 0px 3px 0px rgba(0, 0, 0, 0.5) , 0px 4px 6px 3px rgba(0, 0, 0, 0.3);
|
--panelShadow: 0px 0px 3px 0px rgba(0, 0, 0, 0.5), 0px 4px 6px 3px rgba(0, 0, 0, 0.3);
|
||||||
--panelHeaderShadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4) , 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
--panelHeaderShadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||||
--topBar: rgba(21, 30, 43, 1);
|
--topBar: rgba(21, 30, 43, 1);
|
||||||
--topBarText: rgba(159, 159, 161, 1);
|
--topBarText: rgba(159, 159, 161, 1);
|
||||||
--topBarShadow: 0px 1px 4px 0px rgba(0, 0, 0, 0.4) , 0px 2px 7px 0px rgba(0, 0, 0, 0.3);
|
--topBarShadow: 0px 1px 4px 0px rgba(0, 0, 0, 0.4), 0px 2px 7px 0px rgba(0, 0, 0, 0.3);
|
||||||
--underlay: rgba(9, 14, 20, 0.6);
|
--underlay: rgba(9, 14, 20, 0.6);
|
||||||
--background: rgba(15, 22, 30, 1);
|
--background: rgba(15, 22, 30, 1);
|
||||||
--faint: rgba(185, 185, 186, 0.5);
|
--faint: rgba(185, 185, 186, 0.5);
|
||||||
|
@ -28,9 +28,11 @@ :root {
|
||||||
--border: rgba(26, 37, 53, 1);
|
--border: rgba(26, 37, 53, 1);
|
||||||
--poll: rgba(99, 84, 72, 1);
|
--poll: rgba(99, 84, 72, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
:root {
|
:root {
|
||||||
--icon-filter: invert(67%) sepia(7%) saturate(525%) hue-rotate(173deg) brightness(90%) contrast(92%);;
|
--icon-filter: invert(67%) sepia(7%) saturate(525%) hue-rotate(173deg) brightness(90%) contrast(92%);
|
||||||
|
;
|
||||||
--wallpaper: rgba(248, 250, 252, 1);
|
--wallpaper: rgba(248, 250, 252, 1);
|
||||||
--alertNeutral: rgba(48, 64, 85, 0.5);
|
--alertNeutral: rgba(48, 64, 85, 0.5);
|
||||||
--alertNeutralText: rgba(0, 0, 0, 1);
|
--alertNeutralText: rgba(0, 0, 0, 1);
|
||||||
|
@ -41,10 +43,10 @@ @media (prefers-color-scheme: light) {
|
||||||
--profileTint: rgba(242, 246, 249, 0.5);
|
--profileTint: rgba(242, 246, 249, 0.5);
|
||||||
--btnText: rgba(48, 64, 85, 1);
|
--btnText: rgba(48, 64, 85, 1);
|
||||||
--btn: rgba(214, 223, 237, 1);
|
--btn: rgba(214, 223, 237, 1);
|
||||||
--btnShadow: 0px 0px 2px 0px rgba(0, 0, 0, 0.2) , 0px 1px 0px 0px rgba(255, 255, 255, 0.5) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
--btnShadow: 0px 0px 2px 0px rgba(0, 0, 0, 0.2), 0px 1px 0px 0px rgba(255, 255, 255, 0.5) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
||||||
--btnHoverShadow: 0px 0px 2px 0px rgba(0, 0, 0, 0.2) , 0px 0px 1px 2px rgba(255, 195, 159, 1) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
--btnHoverShadow: 0px 0px 2px 0px rgba(0, 0, 0, 0.2), 0px 0px 1px 2px rgba(255, 195, 159, 1) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
||||||
--lightText: rgba(11, 14, 19, 1);
|
--lightText: rgba(11, 14, 19, 1);
|
||||||
--panelShadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5) , 0px 3px 6px 1px rgba(0, 0, 0, 0.2);
|
--panelShadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5), 0px 3px 6px 1px rgba(0, 0, 0, 0.2);
|
||||||
--panelHeaderShadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.5) inset, 0px 1px 3px 0px rgba(0, 0, 0, 0.3);
|
--panelHeaderShadow: 0px 1px 0px 0px rgba(255, 255, 255, 0.5) inset, 0px 1px 3px 0px rgba(0, 0, 0, 0.3);
|
||||||
--topBar: rgba(214, 223, 237, 1);
|
--topBar: rgba(214, 223, 237, 1);
|
||||||
--topBarText: rgba(48, 64, 85, 1);
|
--topBarText: rgba(48, 64, 85, 1);
|
||||||
|
@ -119,7 +121,7 @@ .inner-nav img {
|
||||||
padding-right: 5px
|
padding-right: 5px
|
||||||
}
|
}
|
||||||
|
|
||||||
body > .container {
|
body>.container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(25em, 45em) 25em;
|
grid-template-columns: minmax(25em, 45em) 25em;
|
||||||
grid-template-areas: "content sidebar";
|
grid-template-areas: "content sidebar";
|
||||||
|
@ -155,6 +157,10 @@ .panel-heading {
|
||||||
box-shadow: var(--panelHeaderShadow);
|
box-shadow: var(--panelHeaderShadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-content {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
.about-content {
|
.about-content {
|
||||||
padding: 0.6em;
|
padding: 0.6em;
|
||||||
}
|
}
|
||||||
|
@ -169,6 +175,18 @@ .sidebar {
|
||||||
padding-left: 0.5em;
|
padding-left: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.column.flex {
|
||||||
|
grid-column-end: sidebar-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scopes-input {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 1em 0;
|
||||||
|
color: var(--muted-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.status-container,
|
.status-container,
|
||||||
.repeat-header,
|
.repeat-header,
|
||||||
.user-card {
|
.user-card {
|
||||||
|
@ -193,6 +211,7 @@ .repeat-header {
|
||||||
.repeat-header .right-side {
|
.repeat-header .right-side {
|
||||||
color: var(--faint);
|
color: var(--faint);
|
||||||
}
|
}
|
||||||
|
|
||||||
.repeat-header .u-photo {
|
.repeat-header .u-photo {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
|
@ -255,6 +274,7 @@ .heading-reply-row {
|
||||||
.reply-to-link {
|
.reply-to-link {
|
||||||
color: var(--faint);
|
color: var(--faint);
|
||||||
}
|
}
|
||||||
|
|
||||||
.reply-to-link:hover {
|
.reply-to-link:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
@ -280,11 +300,13 @@ .h-card {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
header a, .h-card a {
|
header a,
|
||||||
|
.h-card a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
header a:hover, .h-card a:hover {
|
header a:hover,
|
||||||
|
.h-card a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -307,7 +329,7 @@ .attachment {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment > * {
|
.attachment>* {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
@ -322,6 +344,7 @@ .nsfw-banner {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nsfw-banner div {
|
.nsfw-banner div {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -330,6 +353,7 @@ .nsfw-banner div {
|
||||||
.nsfw-banner:not(:hover) {
|
.nsfw-banner:not(:hover) {
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nsfw-banner:hover div {
|
.nsfw-banner:hover div {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -342,10 +366,12 @@ .poll-option {
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.poll-option .percentage {
|
.poll-option .percentage {
|
||||||
width: 3.5em;
|
width: 3.5em;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.poll-option .fill {
|
.poll-option .fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -362,7 +388,8 @@ .status-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-top: 0.75em;
|
margin-top: 0.75em;
|
||||||
}
|
}
|
||||||
.status-actions > * {
|
|
||||||
|
.status-actions>* {
|
||||||
max-width: 4em;
|
max-width: 4em;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -600,7 +627,7 @@ .reply-to-link .fa-icon {
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 800px) {
|
@media (max-width: 800px) {
|
||||||
body > .container {
|
body>.container {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
65
scripts/create_metrics_app.sh
Executable file
65
scripts/create_metrics_app.sh
Executable file
|
@ -0,0 +1,65 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
read -p "Instance URL (e.g https://example.com): " INSTANCE_URL
|
||||||
|
|
||||||
|
echo "Creating oauth app..."
|
||||||
|
|
||||||
|
RESP=$(curl \
|
||||||
|
-XPOST \
|
||||||
|
$INSTANCE_URL/api/v1/apps \
|
||||||
|
--silent \
|
||||||
|
--data-urlencode 'client_name=fedibash' \
|
||||||
|
--data-urlencode 'redirect_uris=urn:ietf:wg:oauth:2.0:oob' \
|
||||||
|
--data-urlencode 'scopes=admin:metrics' \
|
||||||
|
--header "Content-Type: application/x-www-form-urlencoded"
|
||||||
|
)
|
||||||
|
|
||||||
|
client_id=$(echo $RESP | jq -r .client_id)
|
||||||
|
client_secret=$(echo $RESP | jq -r .client_secret)
|
||||||
|
|
||||||
|
if [ -z "$client_id" ]; then
|
||||||
|
echo "Could not create an app"
|
||||||
|
echo "$RESP"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Please visit the following URL and input the code provided"
|
||||||
|
AUTH_URL="$INSTANCE_URL/oauth/authorize?client_id=$client_id&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=admin:metrics&response_type=code"
|
||||||
|
if [ ! -z "$BROWSER" ]; then
|
||||||
|
$BROWSER $AUTH_URL
|
||||||
|
fi;
|
||||||
|
|
||||||
|
echo $AUTH_URL
|
||||||
|
|
||||||
|
read -p "Code: " CODE
|
||||||
|
|
||||||
|
echo "Requesting code..."
|
||||||
|
|
||||||
|
RESP=$(curl \
|
||||||
|
-XPOST \
|
||||||
|
$INSTANCE_URL/oauth/token \
|
||||||
|
--silent \
|
||||||
|
--header "Content-Type: application/x-www-form-urlencoded" \
|
||||||
|
--data-urlencode "client_id=$client_id" \
|
||||||
|
--data-urlencode "client_secret=$client_secret" \
|
||||||
|
--data-urlencode "code=$CODE" \
|
||||||
|
--data-urlencode "grant_type=authorization_code" \
|
||||||
|
--data-urlencode 'redirect_uri=urn:ietf:wg:oauth:2.0:oob' \
|
||||||
|
--data-urlencode "scope=admin:metrics"
|
||||||
|
)
|
||||||
|
echo $RESP
|
||||||
|
ACCESS_TOKEN="$(echo $RESP | jq -r .access_token)"
|
||||||
|
|
||||||
|
echo "Token is $ACCESS_TOKEN"
|
||||||
|
DOMAIN=$(echo $INSTANCE_URL | sed -e 's/^https:\/\///')
|
||||||
|
|
||||||
|
echo "Use the following config in your prometheus.yml:
|
||||||
|
- job_name: akkoma
|
||||||
|
scheme: https
|
||||||
|
authorization:
|
||||||
|
credentials: $ACCESS_TOKEN
|
||||||
|
metrics_path: /api/v1/akkoma/metrics
|
||||||
|
static_configs:
|
||||||
|
- targets:
|
||||||
|
- $DOMAIN
|
||||||
|
"
|
33
test/pleroma/web/akkoma_api/metrics_controller_test.exs
Normal file
33
test/pleroma/web/akkoma_api/metrics_controller_test.exs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
defmodule Pleroma.Web.AkkomaAPI.MetricsControllerTest do
|
||||||
|
use Pleroma.Web.ConnCase, async: true
|
||||||
|
|
||||||
|
describe "GET /api/v1/akkoma/metrics" do
|
||||||
|
test "should return metrics when the user has admin:metrics" do
|
||||||
|
%{conn: conn} = oauth_access(["admin:metrics"])
|
||||||
|
|
||||||
|
resp =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/akkoma/metrics")
|
||||||
|
|> text_response(200)
|
||||||
|
|
||||||
|
assert resp =~ "# HELP"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should not allow users that do not have the admin:metrics scope" do
|
||||||
|
%{conn: conn} = oauth_access(["read:metrics"])
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/akkoma/metrics")
|
||||||
|
|> json_response(403)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should be disabled by export_prometheus_metrics" do
|
||||||
|
clear_config([:instance, :export_prometheus_metrics], false)
|
||||||
|
%{conn: conn} = oauth_access(["admin:metrics"])
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/akkoma/metrics")
|
||||||
|
|> response(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -184,14 +184,15 @@ test "with html format, it redirects to user feed", %{conn: conn} do
|
||||||
note_activity = insert(:note_activity)
|
note_activity = insert(:note_activity)
|
||||||
user = User.get_cached_by_ap_id(note_activity.data["actor"])
|
user = User.get_cached_by_ap_id(note_activity.data["actor"])
|
||||||
|
|
||||||
|
%{assigns: %{csp_nonce: nonce}} = resp_conn = get(conn, "/users/#{user.nickname}")
|
||||||
|
|
||||||
response =
|
response =
|
||||||
conn
|
resp_conn
|
||||||
|> get("/users/#{user.nickname}")
|
|
||||||
|> response(200)
|
|> response(200)
|
||||||
|
|
||||||
assert response ==
|
assert response ==
|
||||||
Pleroma.Web.Fallback.RedirectController.redirector_with_meta(
|
Pleroma.Web.Fallback.RedirectController.redirector_with_meta(
|
||||||
conn,
|
assign(conn, :csp_nonce, nonce),
|
||||||
%{user: user}
|
%{user: user}
|
||||||
).resp_body
|
).resp_body
|
||||||
end
|
end
|
||||||
|
|
|
@ -693,30 +693,25 @@ test "with existing authentication and OOB `redirect_uri`, redirects to app with
|
||||||
|
|
||||||
describe "POST /oauth/authorize" do
|
describe "POST /oauth/authorize" do
|
||||||
test "redirects with oauth authorization, " <>
|
test "redirects with oauth authorization, " <>
|
||||||
"granting requested app-supported scopes to both admin- and non-admin users" do
|
"granting requested app-supported scopes to both admin users" do
|
||||||
app_scopes = ["read", "write", "admin", "secret_scope"]
|
app_scopes = ["read", "write", "admin", "secret_scope"]
|
||||||
app = insert(:oauth_app, scopes: app_scopes)
|
app = insert(:oauth_app, scopes: app_scopes)
|
||||||
redirect_uri = OAuthController.default_redirect_uri(app)
|
redirect_uri = OAuthController.default_redirect_uri(app)
|
||||||
|
|
||||||
non_admin = insert(:user, is_admin: false)
|
|
||||||
admin = insert(:user, is_admin: true)
|
|
||||||
scopes_subset = ["read:subscope", "write", "admin"]
|
scopes_subset = ["read:subscope", "write", "admin"]
|
||||||
|
admin = insert(:user, is_admin: true)
|
||||||
|
|
||||||
# In case scope param is missing, expecting _all_ app-supported scopes to be granted
|
# In case scope param is missing, expecting _all_ app-supported scopes to be granted
|
||||||
for user <- [non_admin, admin],
|
|
||||||
{requested_scopes, expected_scopes} <-
|
|
||||||
%{scopes_subset => scopes_subset, nil: app_scopes} do
|
|
||||||
conn =
|
conn =
|
||||||
post(
|
post(
|
||||||
build_conn(),
|
build_conn(),
|
||||||
"/oauth/authorize",
|
"/oauth/authorize",
|
||||||
%{
|
%{
|
||||||
"authorization" => %{
|
"authorization" => %{
|
||||||
"name" => user.nickname,
|
"name" => admin.nickname,
|
||||||
"password" => "test",
|
"password" => "test",
|
||||||
"client_id" => app.client_id,
|
"client_id" => app.client_id,
|
||||||
"redirect_uri" => redirect_uri,
|
"redirect_uri" => redirect_uri,
|
||||||
"scope" => requested_scopes,
|
"scope" => scopes_subset,
|
||||||
"state" => "statepassed"
|
"state" => "statepassed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -730,8 +725,44 @@ test "redirects with oauth authorization, " <>
|
||||||
assert %{"state" => "statepassed", "code" => code} = query
|
assert %{"state" => "statepassed", "code" => code} = query
|
||||||
auth = Repo.get_by(Authorization, token: code)
|
auth = Repo.get_by(Authorization, token: code)
|
||||||
assert auth
|
assert auth
|
||||||
assert auth.scopes == expected_scopes
|
assert auth.scopes == scopes_subset
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "redirects with oauth authorization, " <>
|
||||||
|
"granting requested app-supported scopes for non-admin users" do
|
||||||
|
app_scopes = ["read", "write", "secret_scope", "admin"]
|
||||||
|
app = insert(:oauth_app, scopes: app_scopes)
|
||||||
|
redirect_uri = OAuthController.default_redirect_uri(app)
|
||||||
|
|
||||||
|
non_admin = insert(:user, is_admin: false)
|
||||||
|
scopes_subset = ["read:subscope", "write"]
|
||||||
|
|
||||||
|
# In case scope param is missing, expecting _all_ app-supported scopes to be granted
|
||||||
|
conn =
|
||||||
|
post(
|
||||||
|
build_conn(),
|
||||||
|
"/oauth/authorize",
|
||||||
|
%{
|
||||||
|
"authorization" => %{
|
||||||
|
"name" => non_admin.nickname,
|
||||||
|
"password" => "test",
|
||||||
|
"client_id" => app.client_id,
|
||||||
|
"redirect_uri" => redirect_uri,
|
||||||
|
"scope" => scopes_subset,
|
||||||
|
"state" => "statepassed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
target = redirected_to(conn)
|
||||||
|
assert target =~ redirect_uri
|
||||||
|
|
||||||
|
query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
|
||||||
|
|
||||||
|
assert %{"state" => "statepassed", "code" => code} = query
|
||||||
|
auth = Repo.get_by(Authorization, token: code)
|
||||||
|
assert auth
|
||||||
|
assert auth.scopes == scopes_subset
|
||||||
end
|
end
|
||||||
|
|
||||||
test "authorize from cookie" do
|
test "authorize from cookie" do
|
||||||
|
@ -831,6 +862,33 @@ test "returns 401 for wrong credentials", %{conn: conn} do
|
||||||
assert result =~ "Invalid Username/Password"
|
assert result =~ "Invalid Username/Password"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "returns 401 when attempting to use an admin scope with a non-admin", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
app = insert(:oauth_app, scopes: ["admin"])
|
||||||
|
redirect_uri = OAuthController.default_redirect_uri(app)
|
||||||
|
|
||||||
|
result =
|
||||||
|
conn
|
||||||
|
|> post("/oauth/authorize", %{
|
||||||
|
"authorization" => %{
|
||||||
|
"name" => user.nickname,
|
||||||
|
"password" => "test",
|
||||||
|
"client_id" => app.client_id,
|
||||||
|
"redirect_uri" => redirect_uri,
|
||||||
|
"state" => "statepassed",
|
||||||
|
"scope" => Enum.join(app.scopes, " ")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|> html_response(:unauthorized)
|
||||||
|
|
||||||
|
# Keep the details
|
||||||
|
assert result =~ app.client_id
|
||||||
|
assert result =~ redirect_uri
|
||||||
|
|
||||||
|
# Error message
|
||||||
|
assert result =~ "outside of authorized scopes"
|
||||||
|
end
|
||||||
|
|
||||||
test "returns 401 for missing scopes" do
|
test "returns 401 for missing scopes" do
|
||||||
user = insert(:user, is_admin: false)
|
user = insert(:user, is_admin: false)
|
||||||
app = insert(:oauth_app, scopes: ["read", "write", "admin"])
|
app = insert(:oauth_app, scopes: ["read", "write", "admin"])
|
||||||
|
@ -855,7 +913,7 @@ test "returns 401 for missing scopes" do
|
||||||
assert result =~ redirect_uri
|
assert result =~ redirect_uri
|
||||||
|
|
||||||
# Error message
|
# Error message
|
||||||
assert result =~ "This action is outside the authorized scopes"
|
assert result =~ "This action is outside of authorized scopes"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns 401 for scopes beyond app scopes hierarchy", %{conn: conn} do
|
test "returns 401 for scopes beyond app scopes hierarchy", %{conn: conn} do
|
||||||
|
@ -882,7 +940,7 @@ test "returns 401 for scopes beyond app scopes hierarchy", %{conn: conn} do
|
||||||
assert result =~ redirect_uri
|
assert result =~ redirect_uri
|
||||||
|
|
||||||
# Error message
|
# Error message
|
||||||
assert result =~ "This action is outside the authorized scopes"
|
assert result =~ "This action is outside of authorized scopes"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -554,7 +554,7 @@ def oauth_app_factory do
|
||||||
%Pleroma.Web.OAuth.App{
|
%Pleroma.Web.OAuth.App{
|
||||||
client_name: sequence(:client_name, &"Some client #{&1}"),
|
client_name: sequence(:client_name, &"Some client #{&1}"),
|
||||||
redirect_uris: "https://example.com/callback",
|
redirect_uris: "https://example.com/callback",
|
||||||
scopes: ["read", "write", "follow", "push", "admin"],
|
scopes: ["read", "write", "follow", "push"],
|
||||||
website: "https://example.com",
|
website: "https://example.com",
|
||||||
client_id: Ecto.UUID.generate(),
|
client_id: Ecto.UUID.generate(),
|
||||||
client_secret: "aaa;/&bbb"
|
client_secret: "aaa;/&bbb"
|
||||||
|
|
Loading…
Reference in a new issue