Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85829574
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
22 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/pleroma/PasswordResetToken.ex b/lib/pleroma/PasswordResetToken.ex
new file mode 100644
index 000000000..79e60bf69
--- /dev/null
+++ b/lib/pleroma/PasswordResetToken.ex
@@ -0,0 +1,44 @@
+defmodule Pleroma.PasswordResetToken do
+ use Ecto.Schema
+
+ import Ecto.{Changeset, Query}
+
+ alias Pleroma.{User, PasswordResetToken, Repo}
+
+ schema "password_reset_tokens" do
+ belongs_to :user, User
+ field :token, :string
+ field :used, :boolean, default: false
+
+ timestamps()
+ end
+
+ def create_token(%User{} = user) do
+ token = :crypto.strong_rand_bytes(32) |> Base.url_encode64
+
+ token = %PasswordResetToken{
+ user_id: user.id,
+ used: false,
+ token: token
+ }
+
+ Repo.insert(token)
+ end
+
+ def used_changeset(struct) do
+ changeset = struct
+ |> cast(%{}, [])
+ |> put_change(:used, true)
+ end
+
+ def reset_password(token, data) do
+ with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}),
+ %User{} = user <- Repo.get(User, token.user_id),
+ {:ok, user} <- User.reset_password(user, data),
+ {:ok, token} <- Repo.update(used_changeset(token)) do
+ {:ok, token}
+ else
+ _e -> {:error, token}
+ end
+ end
+end
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index a04bbe276..938b57d90 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -1,258 +1,277 @@
defmodule Pleroma.User do
use Ecto.Schema
import Ecto.{Changeset, Query}
alias Pleroma.{Repo, User, Object, Web, Activity, Notification}
alias Comeonin.Pbkdf2
alias Pleroma.Web.{OStatus, Websub}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
schema "users" do
field :bio, :string
field :email, :string
field :name, :string
field :nickname, :string
field :password_hash, :string
field :password, :string, virtual: true
field :password_confirmation, :string, virtual: true
field :following, {:array, :string}, default: []
field :ap_id, :string
field :avatar, :map
field :local, :boolean, default: true
field :info, :map, default: %{}
field :follower_address, :string
has_many :notifications, Notification
timestamps()
end
def avatar_url(user) do
case user.avatar do
%{"url" => [%{"href" => href} | _]} -> href
_ -> "https://placehold.it/48x48"
end
end
def banner_url(user) do
case user.info["banner"] do
%{"url" => [%{"href" => href} | _]} -> href
_ -> nil
end
end
def ap_id(%User{nickname: nickname}) do
"#{Web.base_url}/users/#{nickname}"
end
def ap_followers(%User{} = user) do
"#{ap_id(user)}/followers"
end
def follow_changeset(struct, params \\ %{}) do
struct
|> cast(params, [:following])
|> validate_required([:following])
end
def info_changeset(struct, params \\ %{}) do
struct
|> cast(params, [:info])
|> validate_required([:info])
end
def user_info(%User{} = user) do
%{
following_count: length(user.following),
note_count: user.info["note_count"] || 0,
follower_count: user.info["follower_count"] || 0
}
end
@email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
def remote_user_creation(params) do
changes = %User{}
|> cast(params, [:bio, :name, :ap_id, :nickname, :info, :avatar])
|> validate_required([:name, :ap_id, :nickname])
|> unique_constraint(:nickname)
|> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: 5000)
|> validate_length(:name, max: 100)
|> put_change(:local, false)
if changes.valid? do
followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
changes
|> put_change(:follower_address, followers)
else
changes
end
end
def update_changeset(struct, params \\ %{}) do
changeset = struct
|> cast(params, [:bio, :name])
|> unique_constraint(:nickname)
|> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
|> validate_length(:bio, min: 1, max: 1000)
|> validate_length(:name, min: 1, max: 100)
end
+ def password_update_changeset(struct, params) do
+ changeset = struct
+ |> cast(params, [:password, :password_confirmation])
+ |> validate_required([:password, :password_confirmation])
+ |> validate_confirmation(:password)
+
+ if changeset.valid? do
+ hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
+ changeset
+ |> put_change(:password_hash, hashed)
+ else
+ changeset
+ end
+ end
+
+ def reset_password(user, data) do
+ Repo.update(password_update_changeset(user, data))
+ end
+
def register_changeset(struct, params \\ %{}) do
changeset = struct
|> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
|> validate_required([:bio, :email, :name, :nickname, :password, :password_confirmation])
|> validate_confirmation(:password)
|> unique_constraint(:email)
|> unique_constraint(:nickname)
|> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
|> validate_format(:email, @email_regex)
|> validate_length(:bio, min: 1, max: 1000)
|> validate_length(:name, min: 1, max: 100)
if changeset.valid? do
hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
changeset
|> put_change(:password_hash, hashed)
|> put_change(:ap_id, ap_id)
|> put_change(:following, [followers])
|> put_change(:follower_address, followers)
else
changeset
end
end
def follow(%User{} = follower, %User{} = followed) do
ap_followers = followed.follower_address
if following?(follower, followed) do
{:error,
"Could not follow user: #{followed.nickname} is already on your list."}
else
if !followed.local && follower.local do
Websub.subscribe(follower, followed)
end
following = [ap_followers | follower.following]
|> Enum.uniq
follower = follower
|> follow_changeset(%{following: following})
|> Repo.update
{:ok, followed} = update_follower_count(followed)
follower
end
end
def unfollow(%User{} = follower, %User{} = followed) do
ap_followers = followed.follower_address
if following?(follower, followed) do
following = follower.following
|> List.delete(ap_followers)
{ :ok, follower } = follower
|> follow_changeset(%{following: following})
|> Repo.update
{:ok, followed} = update_follower_count(followed)
{:ok, follower, Utils.fetch_latest_follow(follower, followed)}
else
{:error, "Not subscribed!"}
end
end
def following?(%User{} = follower, %User{} = followed) do
Enum.member?(follower.following, followed.follower_address)
end
def get_by_ap_id(ap_id) do
Repo.get_by(User, ap_id: ap_id)
end
def get_cached_by_ap_id(ap_id) do
key = "ap_id:#{ap_id}"
Cachex.get!(:user_cache, key, fallback: fn(_) -> get_by_ap_id(ap_id) end)
end
def get_cached_by_nickname(nickname) do
key = "nickname:#{nickname}"
Cachex.get!(:user_cache, key, fallback: fn(_) -> get_or_fetch_by_nickname(nickname) end)
end
def get_by_nickname(nickname) do
Repo.get_by(User, nickname: nickname)
end
def get_cached_user_info(user) do
key = "user_info:#{user.id}"
Cachex.get!(:user_cache, key, fallback: fn(_) -> user_info(user) end)
end
def get_or_fetch_by_nickname(nickname) do
with %User{} = user <- get_by_nickname(nickname) do
user
else _e ->
with [nick, domain] <- String.split(nickname, "@"),
{:ok, user} <- OStatus.make_user(nickname) do
user
else _e -> nil
end
end
end
# TODO: these queries could be more efficient if the type in postgresql wasn't map, but array.
def get_followers(%User{id: id, follower_address: follower_address}) do
q = from u in User,
where: fragment("? @> ?", u.following, ^follower_address ),
where: u.id != ^id
{:ok, Repo.all(q)}
end
def get_friends(%User{id: id, following: following}) do
q = from u in User,
where: u.follower_address in ^following,
where: u.id != ^id
{:ok, Repo.all(q)}
end
def update_note_count(%User{} = user) do
note_count_query = from a in Object,
where: fragment("? @> ?", a.data, ^%{actor: user.ap_id, type: "Note"}),
select: count(a.id)
note_count = Repo.one(note_count_query)
new_info = Map.put(user.info, "note_count", note_count)
cs = info_changeset(user, %{info: new_info})
Repo.update(cs)
end
def update_follower_count(%User{} = user) do
follower_count_query = from u in User,
where: fragment("? @> ?", u.following, ^user.follower_address),
select: count(u.id)
follower_count = Repo.one(follower_count_query)
new_info = Map.put(user.info, "follower_count", follower_count)
cs = info_changeset(user, %{info: new_info})
Repo.update(cs)
end
def get_notified_from_activity(%Activity{data: %{"to" => to}} = activity) do
query = from u in User,
where: u.ap_id in ^to,
where: u.local == true
Repo.all(query)
end
end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 6abf234c6..8497685a6 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -1,205 +1,215 @@
defmodule Pleroma.Web.Router do
use Pleroma.Web, :router
alias Pleroma.{Repo, User, Web.Router}
def user_fetcher(username) do
{:ok, Repo.get_by(User, %{nickname: username})}
end
pipeline :api do
plug :accepts, ["json"]
plug :fetch_session
plug Pleroma.Plugs.OAuthPlug
plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true}
end
pipeline :authenticated_api do
plug :accepts, ["json"]
plug :fetch_session
plug Pleroma.Plugs.OAuthPlug
plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1}
end
pipeline :well_known do
plug :accepts, ["xml", "xrd+xml"]
end
pipeline :config do
plug :accepts, ["json", "xml"]
end
pipeline :oauth do
plug :accepts, ["html", "json"]
end
+ pipeline :password_reset do
+ plug :accepts, ["html"]
+ end
+
+ scope "/api/pleroma", Pleroma.Web.TwitterAPI do
+ pipe_through :password_reset
+ get "/password_reset/:token", UtilController, :show_password_reset
+ post "/password_reset", UtilController, :password_reset
+ end
+
scope "/oauth", Pleroma.Web.OAuth do
get "/authorize", OAuthController, :authorize
post "/authorize", OAuthController, :create_authorization
post "/token", OAuthController, :token_exchange
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through :authenticated_api
get "/accounts/verify_credentials", MastodonAPIController, :verify_credentials
get "/accounts/relationships", MastodonAPIController, :relationships
post "/accounts/:id/follow", MastodonAPIController, :follow
post "/accounts/:id/unfollow", MastodonAPIController, :unfollow
post "/accounts/:id/block", MastodonAPIController, :relationship_noop
post "/accounts/:id/unblock", MastodonAPIController, :relationship_noop
post "/accounts/:id/mute", MastodonAPIController, :relationship_noop
post "/accounts/:id/unmute", MastodonAPIController, :relationship_noop
get "/blocks", MastodonAPIController, :empty_array
get "/domain_blocks", MastodonAPIController, :empty_array
get "/follow_requests", MastodonAPIController, :empty_array
get "/mutes", MastodonAPIController, :empty_array
get "/timelines/home", MastodonAPIController, :home_timeline
get "/favourites", MastodonAPIController, :favourites
post "/statuses", MastodonAPIController, :post_status
delete "/statuses/:id", MastodonAPIController, :delete_status
post "/statuses/:id/reblog", MastodonAPIController, :reblog_status
post "/statuses/:id/favourite", MastodonAPIController, :fav_status
post "/statuses/:id/unfavourite", MastodonAPIController, :unfav_status
get "/notifications", MastodonAPIController, :notifications
post "/media", MastodonAPIController, :upload
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through :api
get "/instance", MastodonAPIController, :masto_instance
post "/apps", MastodonAPIController, :create_app
get "/timelines/public", MastodonAPIController, :public_timeline
get "/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline
get "/statuses/:id", MastodonAPIController, :get_status
get "/statuses/:id/context", MastodonAPIController, :get_context
get "/statuses/:id/favourited_by", MastodonAPIController, :favourited_by
get "/statuses/:id/reblogged_by", MastodonAPIController, :reblogged_by
get "/accounts/:id/statuses", MastodonAPIController, :user_statuses
get "/accounts/:id/followers", MastodonAPIController, :followers
get "/accounts/:id/following", MastodonAPIController, :following
get "/accounts/:id", MastodonAPIController, :user
get "/search", MastodonAPIController, :search
end
scope "/api", Pleroma.Web do
pipe_through :config
get "/help/test", TwitterAPI.UtilController, :help_test
post "/help/test", TwitterAPI.UtilController, :help_test
get "/statusnet/config", TwitterAPI.UtilController, :config
get "/statusnet/version", TwitterAPI.UtilController, :version
end
@instance Application.get_env(:pleroma, :instance)
@registrations_open Keyword.get(@instance, :registrations_open)
scope "/api", Pleroma.Web do
pipe_through :api
get "/statuses/public_timeline", TwitterAPI.Controller, :public_timeline
get "/statuses/public_and_external_timeline", TwitterAPI.Controller, :public_and_external_timeline
get "/statuses/networkpublic_timeline", TwitterAPI.Controller, :public_and_external_timeline
get "/statuses/user_timeline", TwitterAPI.Controller, :user_timeline
get "/qvitter/statuses/user_timeline", TwitterAPI.Controller, :user_timeline
get "/statuses/show/:id", TwitterAPI.Controller, :fetch_status
get "/statusnet/conversation/:id", TwitterAPI.Controller, :fetch_conversation
if @registrations_open do
post "/account/register", TwitterAPI.Controller, :register
end
get "/search", TwitterAPI.Controller, :search
get "/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline
get "/externalprofile/show", TwitterAPI.Controller, :external_profile
end
scope "/api", Pleroma.Web do
pipe_through :authenticated_api
get "/account/verify_credentials", TwitterAPI.Controller, :verify_credentials
post "/account/verify_credentials", TwitterAPI.Controller, :verify_credentials
post "/account/update_profile", TwitterAPI.Controller, :update_profile
post "/account/update_profile_banner", TwitterAPI.Controller, :update_banner
post "/qvitter/update_background_image", TwitterAPI.Controller, :update_background
post "/account/most_recent_notification", TwitterAPI.Controller, :update_most_recent_notification
get "/statuses/home_timeline", TwitterAPI.Controller, :friends_timeline
get "/statuses/friends_timeline", TwitterAPI.Controller, :friends_timeline
get "/statuses/mentions", TwitterAPI.Controller, :mentions_timeline
get "/statuses/mentions_timeline", TwitterAPI.Controller, :mentions_timeline
post "/statuses/update", TwitterAPI.Controller, :status_update
post "/statuses/retweet/:id", TwitterAPI.Controller, :retweet
post "/statuses/destroy/:id", TwitterAPI.Controller, :delete_post
post "/friendships/create", TwitterAPI.Controller, :follow
post "/friendships/destroy", TwitterAPI.Controller, :unfollow
post "/statusnet/media/upload", TwitterAPI.Controller, :upload
post "/media/upload", TwitterAPI.Controller, :upload_json
post "/favorites/create/:id", TwitterAPI.Controller, :favorite
post "/favorites/create", TwitterAPI.Controller, :favorite
post "/favorites/destroy/:id", TwitterAPI.Controller, :unfavorite
post "/qvitter/update_avatar", TwitterAPI.Controller, :update_avatar
get "/statuses/followers", TwitterAPI.Controller, :followers
get "/statuses/friends", TwitterAPI.Controller, :friends
end
pipeline :ostatus do
plug :accepts, ["xml", "atom", "html"]
end
scope "/", Pleroma.Web do
pipe_through :ostatus
get "/objects/:uuid", OStatus.OStatusController, :object
get "/activities/:uuid", OStatus.OStatusController, :activity
get "/users/:nickname/feed", OStatus.OStatusController, :feed
get "/users/:nickname", OStatus.OStatusController, :feed_redirect
post "/users/:nickname/salmon", OStatus.OStatusController, :salmon_incoming
post "/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request
get "/push/subscriptions/:id", Websub.WebsubController, :websub_subscription_confirmation
post "/push/subscriptions/:id", Websub.WebsubController, :websub_incoming
end
scope "/.well-known", Pleroma.Web do
pipe_through :well_known
get "/host-meta", WebFinger.WebFingerController, :host_meta
get "/webfinger", WebFinger.WebFingerController, :webfinger
end
scope "/", Fallback do
get "/*path", RedirectController, :redirector
end
end
defmodule Fallback.RedirectController do
use Pleroma.Web, :controller
def redirector(conn, _params) do
if Mix.env != :test do
conn
|> put_resp_content_type("text/html")
|> send_file(200, "priv/static/index.html")
end
end
end
diff --git a/lib/pleroma/web/templates/twitter_api/util/invalid_token.html.eex b/lib/pleroma/web/templates/twitter_api/util/invalid_token.html.eex
new file mode 100644
index 000000000..ee84750c7
--- /dev/null
+++ b/lib/pleroma/web/templates/twitter_api/util/invalid_token.html.eex
@@ -0,0 +1 @@
+<h2>Invalid Token</h2>
diff --git a/lib/pleroma/web/templates/twitter_api/util/password_reset.html.eex b/lib/pleroma/web/templates/twitter_api/util/password_reset.html.eex
new file mode 100644
index 000000000..3c7960998
--- /dev/null
+++ b/lib/pleroma/web/templates/twitter_api/util/password_reset.html.eex
@@ -0,0 +1,12 @@
+<h2>Password Reset for <%= @user.nickname %></h2>
+<%= form_for @conn, util_path(@conn, :password_reset), [as: "data"], fn f -> %>
+<%= label f, :password, "Password" %>
+<%= password_input f, :password %>
+<br>
+
+<%= label f, :password_confirmation, "Confirmation" %>
+<%= password_input f, :password_confirmation %>
+<br>
+<%= hidden_input f, :token, value: @token.token %>
+<%= submit "Reset" %>
+<% end %>
diff --git a/lib/pleroma/web/templates/twitter_api/util/password_reset_failed.html.eex b/lib/pleroma/web/templates/twitter_api/util/password_reset_failed.html.eex
new file mode 100644
index 000000000..58a3736fd
--- /dev/null
+++ b/lib/pleroma/web/templates/twitter_api/util/password_reset_failed.html.eex
@@ -0,0 +1 @@
+<h2>Password reset failed</h2>
diff --git a/lib/pleroma/web/templates/twitter_api/util/password_reset_success.html.eex b/lib/pleroma/web/templates/twitter_api/util/password_reset_success.html.eex
new file mode 100644
index 000000000..c7dfcb6dd
--- /dev/null
+++ b/lib/pleroma/web/templates/twitter_api/util/password_reset_success.html.eex
@@ -0,0 +1 @@
+<h2>Password changed!</h2>
diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
index 32910d92c..11d8fa6c2 100644
--- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
@@ -1,49 +1,71 @@
defmodule Pleroma.Web.TwitterAPI.UtilController do
use Pleroma.Web, :controller
alias Pleroma.Web
+ alias Pleroma.{Repo, PasswordResetToken, User}
+
+ def show_password_reset(conn, %{"token" => token}) do
+ with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}),
+ %User{} = user <- Repo.get(User, token.user_id) do
+ render conn, "password_reset.html", %{
+ token: token,
+ user: user
+ }
+ else
+ _e -> render conn, "invalid_token.html"
+ end
+ end
+
+ def password_reset(conn, %{"data" => data}) do
+ with {:ok, _} <- PasswordResetToken.reset_password(data["token"], data) do
+ render conn, "password_reset_success.html"
+ else
+ _e -> render conn, "password_reset_failed.html"
+ end
+ end
+
def help_test(conn, _params) do
json(conn, "ok")
end
@instance Application.get_env(:pleroma, :instance)
def config(conn, _params) do
case get_format(conn) do
"xml" ->
response = """
<config>
<site>
<name>#{Keyword.get(@instance, :name)}</name>
<site>#{Web.base_url}</site>
<textlimit>#{Keyword.get(@instance, :limit)}</textlimit>
<closed>#{!Keyword.get(@instance, :registrations_open)}</closed>
</site>
</config>
"""
conn
|> put_resp_content_type("application/xml")
|> send_resp(200, response)
_ ->
json(conn, %{
site: %{
name: Keyword.get(@instance, :name),
server: Web.base_url,
textlimit: Keyword.get(@instance, :limit),
closed: if(Keyword.get(@instance, :registrations_open), do: "0", else: "1")
}
})
end
end
def version(conn, _params) do
version = Keyword.get(@instance, :version)
case get_format(conn) do
"xml" ->
response = "<version>#{version}</version>"
conn
|> put_resp_content_type("application/xml")
|> send_resp(200, response)
_ -> json(conn, version)
end
end
end
diff --git a/lib/pleroma/web/twitter_api/views/util_view.ex b/lib/pleroma/web/twitter_api/views/util_view.ex
new file mode 100644
index 000000000..71b04e6cc
--- /dev/null
+++ b/lib/pleroma/web/twitter_api/views/util_view.ex
@@ -0,0 +1,4 @@
+defmodule Pleroma.Web.TwitterAPI.UtilView do
+ use Pleroma.Web, :view
+ import Phoenix.HTML.Form
+end
diff --git a/priv/repo/migrations/20171019141706_create_password_reset_tokens.exs b/priv/repo/migrations/20171019141706_create_password_reset_tokens.exs
new file mode 100644
index 000000000..2d9be3aab
--- /dev/null
+++ b/priv/repo/migrations/20171019141706_create_password_reset_tokens.exs
@@ -0,0 +1,13 @@
+defmodule Pleroma.Repo.Migrations.CreatePasswordResetTokens do
+ use Ecto.Migration
+
+ def change do
+ create table(:password_reset_tokens) do
+ add :token, :string
+ add :user_id, references(:users)
+ add :used, :boolean, default: false
+
+ timestamps()
+ end
+ end
+end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Oct 31, 9:24 PM (19 h, 33 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1801836
Default Alt Text
(22 KB)
Attached To
Mode
rPUBE pleroma-upstream
Attached
Detach File
Event Timeline