Allow creating accounts without a password in the AdminAPI, update docs

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk 2026-07-20 19:01:02 +02:00
commit 36bc7b116d
6 changed files with 114 additions and 19 deletions

View file

@ -0,0 +1 @@
Allow creating accounts without a password in the AdminAPI

View file

@ -80,10 +80,24 @@ The `/api/v1/pleroma/admin/*` path is backwards compatible with `/api/pleroma/ad
{
`nickname`,
`email`,
`password`
`password` (optional)
}
]
- Response: Array of user objects
- Response: Array of user objects. When a user is created without a `password`, `data`
object contains a `password_reset_link` the user can follow to set their own password.
```json
[
{
"data": {
"nickname": string,
"email": string,
"password_reset_link": string (optional)
},
"type": "success"
}
]
```
## `POST /api/v1/pleroma/admin/users/follow`

View file

@ -8,13 +8,16 @@ defmodule Pleroma.Web.AdminAPI.UserController do
import Pleroma.Web.ControllerHelper,
only: [fetch_integer_param: 3]
alias Ecto.Multi
alias Pleroma.ModerationLog
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.AdminAPI
alias Pleroma.Web.AdminAPI.Search
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.Router
@users_page_size 50
@ -148,9 +151,24 @@ defmodule Pleroma.Web.AdminAPI.UserController do
} = conn,
_
) do
changesets =
# Nicknames of users for which a password reset link will be generated.
passwordless_nicknames =
users
|> Enum.map(fn %{nickname: nickname, email: email, password: password} ->
|> Enum.filter(fn user -> Map.get(user, :password) in ["", nil] end)
|> Enum.map(& &1.nickname)
multi =
users
|> Enum.map(fn %{nickname: nickname, email: email} = attrs ->
password =
case Map.get(attrs, :password) do
password when is_binary(password) and password != "" ->
password
_ ->
:crypto.strong_rand_bytes(16) |> Base.encode64()
end
user_data = %{
nickname: nickname,
name: nickname,
@ -162,11 +180,11 @@ defmodule Pleroma.Web.AdminAPI.UserController do
User.register_changeset(%User{}, user_data, need_confirmation: false)
end)
|> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
|> Enum.reduce(Multi.new(), fn changeset, multi ->
Multi.insert(multi, "user-" <> changeset.changes.nickname, changeset)
end)
case Pleroma.Repo.transaction(changesets) do
case Pleroma.Repo.transaction(multi) do
{:ok, users_map} ->
users =
users_map
@ -177,17 +195,28 @@ defmodule Pleroma.Web.AdminAPI.UserController do
user
end)
password_reset_links =
users
|> Enum.filter(fn user -> Enum.member?(passwordless_nicknames, user.nickname) end)
|> Map.new(fn user ->
{:ok, token} = Pleroma.PasswordResetToken.create_token(user)
{user.id, Router.Helpers.reset_password_url(Endpoint, :reset, token.token)}
end)
ModerationLog.insert_log(%{
actor: admin,
subjects: users,
action: "create"
})
render(conn, "created_many.json", users: users)
render(conn, "created_many.json",
users: users,
password_reset_links: password_reset_links
)
{:error, id, changeset, _} ->
changesets =
Enum.map(changesets.operations, fn
Enum.map(multi.operations, fn
{^id, {:changeset, _current_changeset, _}} ->
changeset

View file

@ -88,18 +88,29 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
}
end
def render("created_many.json", %{users: users}) do
render_many(users, AccountView, "created.json", as: :user)
def render("created_many.json", %{users: users} = assigns) do
password_reset_links = Map.get(assigns, :password_reset_links, %{})
render_many(users, AccountView, "created.json",
as: :user,
password_reset_links: password_reset_links
)
end
def render("created.json", %{user: user}) do
%{
type: "success",
code: 200,
data: %{
def render("created.json", %{user: user} = assigns) do
password_reset_links = Map.get(assigns, :password_reset_links, %{})
data =
%{
nickname: user.nickname,
email: user.email
}
|> maybe_put_password_reset_link(Map.get(password_reset_links, user.id))
%{
type: "success",
code: 200,
data: data
}
end
@ -119,6 +130,11 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
}
end
defp maybe_put_password_reset_link(data, nil), do: data
defp maybe_put_password_reset_link(data, link),
do: Map.put(data, :password_reset_link, link)
def merge_account_views(%User{} = user) do
MastodonAPI.AccountView.render("show.json", %{user: user, skip_visibility_check: true})
|> Map.merge(AdminAPI.AccountView.render("show.json", %{user: user}))

View file

@ -82,8 +82,14 @@ defmodule Pleroma.Web.ApiSpec.Admin.UserOperation do
properties: %{
nickname: %Schema{type: :string},
email: %Schema{type: :string},
password: %Schema{type: :string}
}
password: %Schema{
type: :string,
description:
"Optional. When omitted, a `password_reset_link` is returned, " <>
"allowing the user to set their own password."
}
},
required: [:nickname, :email]
}
}
}
@ -102,7 +108,8 @@ defmodule Pleroma.Web.ApiSpec.Admin.UserOperation do
type: :object,
properties: %{
email: %Schema{type: :string, format: :email},
nickname: %Schema{type: :string}
nickname: %Schema{type: :string},
password_reset_link: %Schema{type: :string}
}
}
}

View file

@ -176,6 +176,34 @@ defmodule Pleroma.Web.AdminAPI.UserControllerTest do
assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == []
end
test "Creating without a password returns a password reset link", %{conn: conn} do
[account1, account2] =
conn
|> put_req_header("accept", "application/json")
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
%{
"nickname" => "lain",
"email" => "lain@example.org"
},
%{
"nickname" => "lain2",
"email" => "lain2@example.org",
"password" => "dupa.8"
}
]
})
|> json_response_and_validate_schema(200)
assert account1["type"] == "success"
assert %{"password_reset_link" => link} = account1["data"]
assert link =~ "/api/v1/pleroma/password_reset/"
assert account2["type"] == "success"
refute Map.has_key?(account2["data"], "password_reset_link")
end
test "Cannot create user with existing email", %{conn: conn} do
user = insert(:user)