Merge branch '114_email_confirmation' into 'develop'

[#114] Email confirmation

See merge request pleroma/pleroma!546
This commit is contained in:
kaniini 2018-12-20 13:28:36 +00:00
commit 2e2030ada8
21 changed files with 398 additions and 24 deletions

View file

@ -103,8 +103,8 @@ defmodule Mix.Tasks.Pleroma.User do
bio: bio
}
user = User.register_changeset(%User{}, params)
Repo.insert!(user)
changeset = User.register_changeset(%User{}, params, confirmed: true)
{:ok, _user} = User.register(changeset)
Mix.shell().info("User #{nickname} created")

View file

@ -15,6 +15,7 @@ defmodule Pleroma.UserEmail do
defp recipient(email, nil), do: email
defp recipient(email, name), do: {name, email}
defp recipient(%Pleroma.User{} = user), do: recipient(user.email, user.name)
def password_reset_email(user, password_reset_token) when is_binary(password_reset_token) do
password_reset_url =
@ -32,7 +33,7 @@ defmodule Pleroma.UserEmail do
"""
new()
|> to(recipient(user.email, user.name))
|> to(recipient(user))
|> from(sender())
|> subject("Password reset")
|> html_body(html_body)
@ -63,4 +64,26 @@ defmodule Pleroma.UserEmail do
|> subject("Invitation to #{instance_name()}")
|> html_body(html_body)
end
def account_confirmation_email(user) do
confirmation_url =
Router.Helpers.confirm_email_url(
Endpoint,
:confirm_email,
user.id,
to_string(user.info.confirmation_token)
)
html_body = """
<h3>Welcome to #{instance_name()}!</h3>
<p>Email confirmation is required to activate the account.</p>
<p>Click the following link to proceed: <a href="#{confirmation_url}">activate your account</a>.</p>
"""
new()
|> to(recipient(user))
|> from(sender())
|> subject("#{instance_name()} account confirmation")
|> html_body(html_body)
end
end

View file

@ -38,6 +38,13 @@ defmodule Pleroma.User do
timestamps()
end
def auth_active?(%User{} = user) do
(user.info && !user.info.confirmation_pending) ||
!Pleroma.Config.get([:instance, :account_activation_required])
end
def superuser?(%User{} = user), do: user.info && User.Info.superuser?(user.info)
def avatar_url(user) do
case user.avatar do
%{"url" => [%{"href" => href} | _]} -> href
@ -78,6 +85,7 @@ defmodule Pleroma.User do
note_count: user.info.note_count,
follower_count: user.info.follower_count,
locked: user.info.locked,
confirmation_pending: user.info.confirmation_pending,
default_scope: user.info.default_scope
}
end
@ -168,7 +176,16 @@ defmodule Pleroma.User do
update_and_set_cache(password_update_changeset(user, data))
end
def register_changeset(struct, params \\ %{}) do
def register_changeset(struct, params \\ %{}, opts \\ []) do
confirmation_status =
if opts[:confirmed] || !Pleroma.Config.get([:instance, :account_activation_required]) do
:confirmed
else
:unconfirmed
end
info_change = User.Info.confirmation_changeset(%User.Info{}, confirmation_status)
changeset =
struct
|> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
@ -180,7 +197,7 @@ defmodule Pleroma.User do
|> validate_format(:email, @email_regex)
|> validate_length(:bio, max: 1000)
|> validate_length(:name, min: 1, max: 100)
|> put_change(:info, %Pleroma.User.Info{})
|> put_change(:info, info_change)
if changeset.valid? do
hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
@ -197,6 +214,25 @@ defmodule Pleroma.User do
end
end
@doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
def register(%Ecto.Changeset{} = changeset) do
with {:ok, user} <- Repo.insert(changeset),
{:ok, _} = try_send_confirmation_email(user) do
{:ok, user}
end
end
def try_send_confirmation_email(%User{} = user) do
if user.info.confirmation_pending &&
Pleroma.Config.get([:instance, :account_activation_required]) do
user
|> Pleroma.UserEmail.account_confirmation_email()
|> Pleroma.Mailer.deliver()
else
{:ok, :noop}
end
end
def needs_update?(%User{local: true}), do: false
def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true

View file

@ -9,6 +9,8 @@ defmodule Pleroma.User.Info do
field(:note_count, :integer, default: 0)
field(:follower_count, :integer, default: 0)
field(:locked, :boolean, default: false)
field(:confirmation_pending, :boolean, default: false)
field(:confirmation_token, :string, default: nil)
field(:default_scope, :string, default: "public")
field(:blocks, {:array, :string}, default: [])
field(:domain_blocks, {:array, :string}, default: [])
@ -35,6 +37,8 @@ defmodule Pleroma.User.Info do
# subject _> Where is this used?
end
def superuser?(info), do: info.is_admin || info.is_moderator
def set_activation_status(info, deactivated) do
params = %{deactivated: deactivated}
@ -141,6 +145,24 @@ defmodule Pleroma.User.Info do
])
end
def confirmation_changeset(info, :confirmed) do
confirmation_changeset(info, %{
confirmation_pending: false,
confirmation_token: nil
})
end
def confirmation_changeset(info, :unconfirmed) do
confirmation_changeset(info, %{
confirmation_pending: true,
confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
})
end
def confirmation_changeset(info, params) do
cast(info, params, [:confirmation_pending, :confirmation_token])
end
def mastodon_profile_update(info, params) do
info
|> cast(params, [

View file

@ -1,6 +1,6 @@
defmodule Pleroma.Web.AdminAPI.AdminAPIController do
use Pleroma.Web, :controller
alias Pleroma.{User, Repo}
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Relay
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
@ -26,7 +26,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
conn,
%{"nickname" => nickname, "email" => email, "password" => password}
) do
new_user = %{
user_data = %{
nickname: nickname,
name: nickname,
email: email,
@ -35,11 +35,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
bio: "."
}
User.register_changeset(%User{}, new_user)
|> Repo.insert!()
changeset = User.register_changeset(%User{}, user_data, confirmed: true)
{:ok, user} = User.register(changeset)
conn
|> json(new_user.nickname)
|> json(user.nickname)
end
def tag_users(conn, %{"nicknames" => nicknames, "tags" => tags}) do

View file

@ -110,7 +110,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def user(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
with %User{} = user <- Repo.get(User, id) do
with %User{} = user <- Repo.get(User, id),
true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
account = AccountView.render("account.json", %{user: user, for: for_user})
json(conn, account)
else

View file

@ -62,6 +62,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
# Pleroma extension
pleroma: %{
confirmation_pending: user_info.confirmation_pending,
tags: user.tags
}
}

View file

@ -132,6 +132,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
banner: Keyword.get(instance, :banner_upload_limit),
background: Keyword.get(instance, :background_upload_limit)
},
accountActivationRequired: Keyword.get(instance, :account_activation_required, false),
invitesEnabled: Keyword.get(instance, :invites_enabled, false),
features: features
}

View file

@ -31,6 +31,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
}) do
with %User{} = user <- User.get_by_nickname_or_email(name),
true <- Pbkdf2.checkpw(password, user.password_hash),
{:auth_active, true} <- {:auth_active, User.auth_active?(user)},
%App{} = app <- Repo.get_by(App, client_id: client_id),
{:ok, auth} <- Authorization.create_authorization(app, user) do
# Special case: Local MastodonFE.
@ -63,6 +64,15 @@ defmodule Pleroma.Web.OAuth.OAuthController do
redirect(conn, external: url)
end
else
{:auth_active, false} ->
conn
|> put_flash(:error, "Account confirmation pending")
|> put_status(:forbidden)
|> authorize(params)
error ->
error
end
end
@ -101,6 +111,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
with %App{} = app <- get_app_from_request(conn, params),
%User{} = user <- User.get_by_nickname_or_email(name),
true <- Pbkdf2.checkpw(password, user.password_hash),
{:auth_active, true} <- {:auth_active, User.auth_active?(user)},
{:ok, auth} <- Authorization.create_authorization(app, user),
{:ok, token} <- Token.exchange_token(app, auth) do
response = %{
@ -113,6 +124,11 @@ defmodule Pleroma.Web.OAuth.OAuthController do
json(conn, response)
else
{:auth_active, false} ->
conn
|> put_status(:forbidden)
|> json(%{error: "Account confirmation pending"})
_error ->
put_status(conn, 400)
|> json(%{error: "Invalid credentials"})

View file

@ -283,6 +283,15 @@ defmodule Pleroma.Web.Router do
post("/account/register", TwitterAPI.Controller, :register)
post("/account/password_reset", TwitterAPI.Controller, :password_reset)
get(
"/account/confirm_email/:user_id/:token",
TwitterAPI.Controller,
:confirm_email,
as: :confirm_email
)
post("/account/resend_confirmation_email", TwitterAPI.Controller, :resend_confirmation_email)
get("/search", TwitterAPI.Controller, :search)
get("/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline)
end

View file

@ -174,6 +174,8 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
closed: if(Keyword.get(instance, :registrations_open), do: "0", else: "1"),
private: if(Keyword.get(instance, :public, true), do: "0", else: "1"),
vapidPublicKey: vapid_public_key,
accountActivationRequired:
if(Keyword.get(instance, :account_activation_required, false), do: "1", else: "0"),
invitesEnabled: if(Keyword.get(instance, :invites_enabled, false), do: "1", else: "0")
}

View file

@ -1,8 +1,10 @@
defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
alias Pleroma.{UserInviteToken, User, Activity, Repo, Object}
alias Pleroma.{UserEmail, Mailer}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.TwitterAPI.UserView
alias Pleroma.Web.CommonAPI
import Ecto.Query
def create_status(%User{} = user, %{"status" => _} = data) do
@ -161,10 +163,11 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
cond do
registrations_open || (!is_nil(token) && !token.used) ->
changeset = User.register_changeset(%User{info: %{}}, params)
changeset = User.register_changeset(%User{}, params)
with {:ok, user} <- Repo.insert(changeset) do
with {:ok, user} <- User.register(changeset) do
!registrations_open && UserInviteToken.mark_as_used(token.token)
{:ok, user}
else
{:error, changeset} ->
@ -189,8 +192,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
%User{local: true} = user <- User.get_by_nickname_or_email(nickname_or_email),
{:ok, token_record} <- Pleroma.PasswordResetToken.create_token(user) do
user
|> Pleroma.UserEmail.password_reset_email(token_record.token)
|> Pleroma.Mailer.deliver()
|> UserEmail.password_reset_email(token_record.token)
|> Mailer.deliver()
else
false ->
{:error, "bad user identifier"}

View file

@ -96,10 +96,15 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end
def show_user(conn, params) do
with {:ok, shown} <- TwitterAPI.get_user(params) do
for_user = conn.assigns.user
with {:ok, shown} <- TwitterAPI.get_user(params),
true <-
User.auth_active?(shown) ||
(for_user && (for_user.id == shown.id || User.superuser?(for_user))) do
params =
if user = conn.assigns.user do
%{user: shown, for: user}
if for_user do
%{user: shown, for: for_user}
else
%{user: shown}
end
@ -110,6 +115,11 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
else
{:error, msg} ->
bad_request_reply(conn, msg)
false ->
conn
|> put_status(404)
|> json(%{error: "Unconfirmed user"})
end
end
@ -372,6 +382,29 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end
end
def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
with %User{} = user <- Repo.get(User, uid),
true <- user.local,
true <- user.info.confirmation_pending,
true <- user.info.confirmation_token == token,
info_change <- User.Info.confirmation_changeset(user.info, :confirmed),
changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_change),
{:ok, _} <- User.update_and_set_cache(changeset) do
conn
|> redirect(to: "/")
end
end
def resend_confirmation_email(conn, params) do
nickname_or_email = params["email"] || params["nickname"]
with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
{:ok, _} <- User.try_send_confirmation_email(user) do
conn
|> json_response(:no_content, "")
end
end
def update_avatar(%{assigns: %{user: user}} = conn, params) do
{:ok, object} = ActivityPub.upload(params, type: :avatar)
change = Changeset.change(user, %{avatar: object.data})

View file

@ -81,6 +81,7 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
# Pleroma extension
"pleroma" => %{
"confirmation_pending" => user_info.confirmation_pending,
"tags" => user.tags
}
}