Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge develop to staging #133

Merged
merged 17 commits into from
Dec 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .iex.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use Timex
alias SingForNeeds.Accounts.User
alias SingForNeeds.Artists
alias SingForNeeds.Artists.Artist
alias SingForNeeds.Causes
Expand Down
109 changes: 109 additions & 0 deletions lib/sing_for_needs/accounts.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
defmodule SingForNeeds.Accounts do
@moduledoc """
The Accounts context.
"""

import Ecto.Query, warn: false
alias SingForNeeds.Repo

alias SingForNeeds.Accounts.User

@doc """
Returns the list of users.

## Examples

iex> list_users()
[%User{}, ...]

"""
def list_users do
Repo.all(User)
end

@doc """
Gets a single user.

Raises `Ecto.NoResultsError` if the User does not exist.

## Examples

iex> get_user!(123)
%User{}

iex> get_user!(456)
** (Ecto.NoResultsError)

"""
def get_user!(id), do: Repo.get!(User, id)

@doc """
returns {:ok, user}
"""
def get_user(id), do: Repo.get!(User, id)

@doc """
Creates a user.

## Examples

iex> create_user(%{field: value})
{:ok, %User{}}

iex> create_user(%{field: bad_value})
{:error, %Ecto.Changeset{}}

"""
def create_user(attrs \\ %{}) do
%User{}
|> User.changeset(attrs)
|> Repo.insert()
end

@doc """
Updates a user.

## Examples

iex> update_user(user, %{field: new_value})
{:ok, %User{}}

iex> update_user(user, %{field: bad_value})
{:error, %Ecto.Changeset{}}

"""
def update_user(%User{} = user, attrs) do
user
|> User.changeset(attrs)
|> Repo.update()
end

@doc """
Deletes a User.

## Examples

iex> delete_user(user)
{:ok, %User{}}

iex> delete_user(user)
{:error, %Ecto.Changeset{}}

"""
def delete_user(%User{} = user) do
Repo.delete(user)
end

@doc """
Returns an `%Ecto.Changeset{}` for tracking user changes.

## Examples

iex> change_user(user)
%Ecto.Changeset{source: %User{}}

"""
def change_user(%User{} = user) do
User.changeset(user, %{})
end
end
39 changes: 39 additions & 0 deletions lib/sing_for_needs/accounts/user.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
defmodule SingForNeeds.Accounts.User do
@moduledoc """
User schema
"""
use Ecto.Schema
import Ecto.Changeset

schema "users" do
field :avatar_url, :string
field :email, :string
field :password, :string, virtual: true
field :password_hash, :string
field :username, :string

timestamps()
end

@doc false
def changeset(user, attrs) do
user
|> cast(attrs, [:username, :email, :password, :password_hash, :avatar_url])
|> validate_required([:username, :email, :password])
|> validate_length(:username, min: 2)
|> validate_length(:password, min: 6)
|> unique_constraint(:username)
|> unique_constraint(:email)
|> hash_password()
end

defp hash_password(changeset) do
case changeset do
%Ecto.Changeset{valid?: true, changes: %{password: password}} ->
put_change(changeset, :password_hash, Pbkdf2.hash_pwd_salt(password))

_ ->
changeset
end
end
end
45 changes: 20 additions & 25 deletions lib/sing_for_needs/causes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -43,35 +43,30 @@ defmodule SingForNeeds.Causes do
|> Repo.all()
end

defp causes_query(%{limit: limit}) do
limit(Cause, ^limit)
defp causes_query(criteria) do
query = from c in Cause, select: c
Enum.reduce(criteria, query, &compose_query/2)
end

defp causes_query(%{scope: scope}) do
case scope do
"trending" ->
from(c in Cause,
left_join: a in assoc(c, :artists),
preload: [:artists],
order_by: [desc: :amount_raised, desc: count(a.id)],
group_by: c.id,
select: c
)

"ending_soon" ->
from(c in Cause,
where: c.end_date > ^Timex.now(),
order_by: [asc: c.end_date],
select: c
)
end
defp compose_query({:scope, "trending"}, query) do
from c in query,
preload: [:artists],
left_join: a in assoc(c, :artists),
order_by: [desc: :amount_raised, desc: count(a.id)],
group_by: c.id
end

defp causes_query(criteria) do
Enum.reduce(criteria, Cause, fn
{:order, order}, query ->
order_by(query, {^order, :name})
end)
defp compose_query({:scope, "ending_soon"}, query) do
ninety_days_from_now = Timex.add(Timex.now(), Timex.Duration.from_days(90))

from c in query,
preload: [:artists],
where: c.end_date > ^Timex.now() and c.end_date <= ^ninety_days_from_now,
order_by: [asc: c.end_date]
end

defp compose_query({:limit, limit}, query) do
limit(query, ^limit)
end

@doc """
Expand Down
22 changes: 22 additions & 0 deletions lib/sing_for_needs_web/auth_token.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
defmodule SingForNeedsWeb.AuthToken do
@moduledoc """
Implements assignment of a token to a user requesting access and verification
wheter the token provided by a user is valid
"""
@user_salt "user salt"
@doc """
Encodes a given user id, signs it returning a token
Clients can use as identification when using the API
"""
def sign(user) do
Phoenix.Token.sign(SingForNeedsWeb.Endpoint, @user_salt, %{id: user.id})
end

@doc """
Decodes original data from the given token,
and verifies its integrity
"""
def verify(token) do
Phoenix.Token.verify(SingForNeedsWeb.Endpoint, @user_salt, token, max_age: 365 * 24 * 3600)
end
end
29 changes: 29 additions & 0 deletions lib/sing_for_needs_web/resolvers/accounts.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
defmodule SingForNeedsWeb.Resolvers.Accounts do
@moduledoc """
Resolvers for User
"""
alias SingForNeeds.Accounts

def signup(_parent, args, _resolution) do
case Accounts.create_user(args) do
{:error, _changeset} ->
{
:error,
message: "Could not create account"
# details: SingForNeedsWeb.Schema.ChangesetErrors.error_details(changeset)
}

{:ok, user} ->
token = SingForNeedsWeb.AuthToken.sign(user)
{:ok, %{token: token, user: user}}
end
end

def me(_parent, _args, %{context: %{current_user: user}}) do
{:ok, user}
end

def me(_parent, _args, _resolution) do
{:ok, nil}
end
end
1 change: 1 addition & 0 deletions lib/sing_for_needs_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ defmodule SingForNeedsWeb.Router do
pipeline :api do
plug CORSPlug, origin: ["http://localhost:3000", "http://127.0.0.1:3000"]
plug :accepts, ["json"]
plug SingForNeedsWeb.Plugs.SetCurrentUser
end

scope "/" do
Expand Down
28 changes: 26 additions & 2 deletions lib/sing_for_needs_web/schema/schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,16 @@ defmodule SingForNeedsWeb.Schema.Schema do
"""
use Absinthe.Schema
import_types(Absinthe.Type.Custom)
import_types(SingForNeedsWeb.Schema.{ArtistTypes, CauseTypes, PerformanceTypes})
alias SingForNeedsWeb.Resolvers.{Artist, Cause, Performance}

import_types(SingForNeedsWeb.Schema.{
ArtistTypes,
CauseTypes,
PerformanceTypes,
SessionTypes,
UserTypes
})

alias SingForNeedsWeb.Resolvers.{Accounts, Artist, Cause, Performance}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the time you are using the multiple single line alias/require/import/use directives but here you are using the multi-alias/require/import/use syntax


def dataloader do
alias SingForNeeds.{Artists, Causes}
Expand Down Expand Up @@ -40,6 +48,11 @@ defmodule SingForNeedsWeb.Schema.Schema do
field :performances, list_of(:performance) do
resolve(&Performance.performances/3)
end

@desc "get my user details"
field :me, :user do
resolve(&Accounts.me/3)
end
end

mutation do
Expand Down Expand Up @@ -77,5 +90,16 @@ defmodule SingForNeedsWeb.Schema.Schema do
arg(:bio, :string)
resolve(&Artist.update_artist/3)
end

@doc """
signup mutation
"""
field :signup, :session do
arg(:username, non_null(:string))
arg(:email, non_null(:string))
arg(:password, non_null(:string))
arg(:avatar_url, :string)
resolve(&Accounts.signup/3)
end
end
end
11 changes: 11 additions & 0 deletions lib/sing_for_needs_web/schema/session_types.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
defmodule SingForNeedsWeb.Schema.SessionTypes do
@moduledoc """
All types for user
"""
use Absinthe.Schema.Notation

object :session do
field :token, non_null(:string)
field :user, non_null(:user)
end
end
13 changes: 13 additions & 0 deletions lib/sing_for_needs_web/schema/user_types.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
defmodule SingForNeedsWeb.Schema.UserTypes do
@moduledoc """
All types for user
"""
use Absinthe.Schema.Notation

object :user do
field :id, :id
field :avatar_url, :string
field :email, :string
field :username, :string
end
end
33 changes: 33 additions & 0 deletions lib/sing_for_needs_web/set_current_user.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Adds an Absinthe execution context to the Phoenix connection.
# If a valid auth token is in the request header, the corresponding
# user is added to the context which is then available to all
# resolvers. Otherwise, the context is empty.
#
# This plug runs prior to `Absinthe.Plug` in the `:api` router
# so that the context is set up and `Absinthe.Plug` can extract
# the context from the connection.

defmodule SingForNeedsWeb.Plugs.SetCurrentUser do
@moduledoc """
Plug to set the current user
"""
@behaviour Plug
import Plug.Conn

def init(opts), do: opts

def call(conn, _) do
context = build_context(conn)
Absinthe.Plug.put_options(conn, context: context)
end

defp build_context(conn) do
with ["Bearer " <> token] <- get_req_header(conn, "authorization"),
{:ok, %{id: id}} <- SingForNeedsWeb.AuthToken.verify(token),
%{} = user <- SingForNeeds.Accounts.get_user(id) do
%{current_user: user}
else
_ -> %{}
end
end
end
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ defmodule SingForNeeds.MixProject do
{:ex_machina, "~> 2.3", only: :test},
{:excoveralls, "~> 0.10", only: :test},
{:timex, "~> 3.5"},
{:faker, "~> 0.13", only: :test}
{:faker, "~> 0.13", only: :test},
{:pbkdf2_elixir, "~> 1.0"}
]
end

Expand Down