Commit 13c3ee23 authored by James Laver's avatar James Laver

Merge branch 'feature/unique-usernames' into 'develop'

Feature/unique usernames

See merge request !14
parents 99a47a3e a64b0034
Pipeline #64844949 passed with stages
in 8 minutes and 32 seconds
......@@ -8,9 +8,10 @@ defmodule MoodleNet.Accounts do
User Accounts context
"""
require Ecto.Query
alias MoodleNet.Repo
alias Ecto.Multi
alias Ecto.Query, as: EQuery
alias MoodleNet.Accounts.{
User,
PasswordAuth,
......@@ -19,7 +20,7 @@ defmodule MoodleNet.Accounts do
WhitelistEmail
}
alias MoodleNet.{Mailer, Email}
alias MoodleNet.{Mailer, Email, Token, Gravatar}
alias ActivityPub.SQL.{Alter, Query}
......@@ -203,7 +204,7 @@ defmodule MoodleNet.Accounts do
end
defp renew_reset_password_token(user) do
changeset = MoodleNet.Accounts.ResetPasswordToken.build_changeset(user)
changeset = ResetPasswordToken.build_changeset(user)
opts = [returning: true, on_conflict: :replace_all, conflict_target: :user_id]
Repo.insert(changeset, opts)
end
......@@ -234,7 +235,7 @@ defmodule MoodleNet.Accounts do
end
defp get_reset_password_token(full_token) do
with {:ok, {user_id, _}} <- MoodleNet.Token.split_id_and_token(full_token),
with {:ok, {user_id, _}} <- Token.split_id_and_token(full_token),
ret = %{token: rp_token} <- Repo.get_by(ResetPasswordToken, user_id: user_id),
false <- expired_token?(ret),
^full_token <- rp_token do
......@@ -244,6 +245,15 @@ defmodule MoodleNet.Accounts do
end
end
def is_username_available?(username) do
ret =
"activity_pub_actor_aspects"
|> EQuery.where([a], a.preferred_username == ^username)
|> Repo.aggregate(:count, :local_id)
ret == 0
end
@two_days 60 * 60 * 24 * 2
defp expired_token?(%{inserted_at: date}) do
NaiveDateTime.diff(NaiveDateTime.utc_now(), date) > @two_days
......@@ -262,7 +272,7 @@ defmodule MoodleNet.Accounts do
end
defp get_email_confirmation_token(full_token) do
with {:ok, {user_id, _}} <- MoodleNet.Token.split_id_and_token(full_token),
with {:ok, {user_id, _}} <- Token.split_id_and_token(full_token),
ret = %{token: ec_token} <- Repo.get_by(EmailConfirmationToken, user_id: user_id),
^full_token <- ec_token do
{:ok, ret}
......@@ -289,7 +299,7 @@ defmodule MoodleNet.Accounts do
defp set_default_icon(attrs) do
if email = attrs["email"] || attrs[:email] do
Map.put(attrs, :icon, %{type: "Image", url: MoodleNet.Gravatar.url(email)})
Map.put(attrs, :icon, %{type: "Image", url: Gravatar.url(email)})
else
attrs
end
......
......@@ -5,8 +5,9 @@
defmodule MoodleNetWeb.GraphQL.UserResolver do
import MoodleNetWeb.GraphQL.MoodleNetSchema
alias MoodleNetWeb.GraphQL.Errors
require ActivityPub.Guards, as: APG
alias MoodleNetWeb.GraphQL.Errors
alias MoodleNet.{Accounts, OAuth, Repo}
def me(_, info) do
with {:ok, actor} <- current_actor(info) do
......@@ -27,8 +28,8 @@ defmodule MoodleNetWeb.GraphQL.UserResolver do
def create_user(%{user: attrs}, info) do
attrs = attrs |> set_icon() |> set_image() |> set_location() |> set_website()
with {:ok, %{actor: actor, user: user}} <- MoodleNet.Accounts.register_user(attrs),
{:ok, token} <- MoodleNet.OAuth.create_token(user.id) do
with {:ok, %{actor: actor, user: user}} <- Accounts.register_user(attrs),
{:ok, token} <- OAuth.create_token(user.id) do
auth_payload = prepare(:auth_payload, token, actor, info)
{:ok, auth_payload}
end
......@@ -37,7 +38,7 @@ defmodule MoodleNetWeb.GraphQL.UserResolver do
def update_profile(%{profile: attrs}, info) do
with {:ok, current_actor} <- current_actor(info),
{:ok, current_actor} <- MoodleNet.Accounts.update_user(current_actor, attrs) do
{:ok, current_actor} <- Accounts.update_user(current_actor, attrs) do
user_fields = requested_fields(info, :user)
current_actor = prepare(:me, current_actor, user_fields)
{:ok, current_actor}
......@@ -47,14 +48,14 @@ defmodule MoodleNetWeb.GraphQL.UserResolver do
def delete_user(_, info) do
with {:ok, current_actor} <- current_actor(info) do
MoodleNet.Accounts.delete_user(current_actor)
Accounts.delete_user(current_actor)
{:ok, true}
end
end
def create_session(%{email: email, password: password}, info) do
with {:ok, user} <- MoodleNet.Accounts.authenticate_by_email_and_pass(email, password),
{:ok, token} <- MoodleNet.OAuth.create_token(user.id) do
with {:ok, user} <- Accounts.authenticate_by_email_and_pass(email, password),
{:ok, token} <- OAuth.create_token(user.id) do
actor = load_actor(user)
auth_payload = prepare(:auth_payload, token, actor, info)
{:ok, auth_payload}
......@@ -66,26 +67,29 @@ defmodule MoodleNetWeb.GraphQL.UserResolver do
def delete_session(_, info) do
with {:ok, _} <- current_user(info) do
MoodleNet.OAuth.revoke_token(info.context.auth_token)
OAuth.revoke_token(info.context.auth_token)
{:ok, true}
end
end
def check_username_available(%{username: username}, _info),
do: {:ok, Accounts.is_username_available?(username)}
def reset_password_request(%{email: email}, _info) do
# Note: This can be done async, but then, the async tests will fail
MoodleNet.Accounts.reset_password_request(email)
Accounts.reset_password_request(email)
{:ok, true}
end
def reset_password(%{token: token, password: password}, _info) do
with {:ok, _} <- MoodleNet.Accounts.reset_password(token, password) do
with {:ok, _} <- Accounts.reset_password(token, password) do
{:ok, true}
end
|> Errors.handle_error()
end
def confirm_email(%{token: token}, _info) do
with {:ok, _} <- MoodleNet.Accounts.confirm_email(token) do
with {:ok, _} <- Accounts.confirm_email(token) do
{:ok, true}
end
|> Errors.handle_error()
......@@ -104,4 +108,5 @@ defmodule MoodleNetWeb.GraphQL.UserResolver do
|> preload_aspect_cond([:actor_aspect], fields)
|> prepare_common_fields()
end
end
......@@ -21,6 +21,12 @@ defmodule MoodleNetWeb.GraphQL.UserSchema do
arg(:local_id, non_null(:integer))
resolve(&UserResolver.user/2)
end
@desc "Check if a user exists with a username"
field :username_available, type: :boolean do
arg(:username, non_null(:string))
resolve(&UserResolver.check_username_available/2)
end
end
object :user_mutations do
......
# MoodleNet: Connecting and empowering educators worldwide
# Copyright © 2018-2019 Moodle Pty Ltd <https://moodle.com/moodlenet/>
# Contains code from Pleroma <https://pleroma.social/> and CommonsPub <https://commonspub.org/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule MoodleNet.Repo.Migrations.MakePreferredUsernameUnique do
use ActivityPub.Migration
def change do
create unique_index(:activity_pub_actor_aspects, :preferred_username)
end
end
......@@ -20,6 +20,8 @@ defmodule MoodleNet.AccountsTest do
|> Map.put("image", image_attrs)
|> Map.put("extra_field", "extra")
assert true == Accounts.is_username_available?(attrs["preferred_username"])
Accounts.add_email_to_whitelist(attrs["email"])
assert {:ok, ret} = Accounts.register_user(attrs)
assert attrs["email"] == ret.user.email
......@@ -33,6 +35,8 @@ defmodule MoodleNet.AccountsTest do
assert [image_attrs["url"]] == get_in(ret, [:actor, :image, Access.at(0), :url])
assert_delivered_email(MoodleNet.Email.welcome(ret.user, ret.email_confirmation_token.token))
assert false == Accounts.is_username_available?(ret.actor.preferred_username)
end
test "works with moodle.com emails" do
......
......@@ -288,6 +288,35 @@ defmodule MoodleNetWeb.GraphQL.UserSchemaTest do
assert user["primaryLanguage"] == actor["primary_language"]
end
@tag :user
test "check if a preferred username is taken", %{conn: conn, actor: actor} do
query = """
{
usernameAvailable(username: "jameslaver")
}
"""
assert true ==
conn
|> post("/api/graphql", %{query: query})
|> json_response(200)
|> Map.fetch!("data")
|> Map.fetch!("usernameAvailable")
query = """
{
usernameAvailable(username: "#{actor.preferred_username}")
}
"""
assert false ==
conn
|> post("/api/graphql", %{query: query})
|> json_response(200)
|> Map.fetch!("data")
|> Map.fetch!("usernameAvailable")
end
@tag :user
test "joined_communities connection", %{conn: conn, actor: actor} do
local_id = local_id(actor)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment