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

Update user accounts, add user sign-in and auth for admin dashboard #148

Merged
merged 16 commits into from
Jul 18, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion server/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
/deps
/*.ez

/priv/static/
/rel

node_modules/
Expand Down
3 changes: 3 additions & 0 deletions server/assets/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ module.exports = {
'../lib/*_web/**/*.*ex',
"../deps/ash_authentication_phoenix/**/*.ex"
],
// Enable dark mode with a 'dark' class on html element
// See: https://tailwindcss.com/docs/dark-mode
darkMode: 'class',
theme: {
extend: {},
},
Expand Down
9 changes: 9 additions & 0 deletions server/compile
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,14 @@

source $phoenix_dir/load_env.sh

echo "---- Building UI, running: npm run build"

cd $phoenix_dir/ui
npm run build

echo "---- UI build done, running: mix assets.deploy"

cd $phoenix_dir
mix assets.deploy

echo "---- mix assets.deploy done"
3 changes: 1 addition & 2 deletions server/config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ config :orcasite,

# Configures the endpoint
config :orcasite, OrcasiteWeb.Endpoint,
url: [host: "localhost"],
render_errors: [
formats: [html: OrcasiteWeb.ErrorHTML, json: OrcasiteWeb.ErrorJSON],
layout: false
Expand Down Expand Up @@ -74,7 +73,7 @@ config :orcasite, OrcasiteWeb.Auth.AuthAccessPipeline,
error_handler: OrcasiteWeb.Auth.AuthErrorHandler

config :ash, :use_all_identities_in_manage_relationship?, false
config :orcasite, :ash_apis, [Orcasite.Notifications]
config :orcasite, :ash_apis, [Orcasite.Notifications, Orcasite.Accounts]
config :orcasite, :ecto_repos, [Orcasite.Repo]

config :orcasite, Oban,
Expand Down
1 change: 1 addition & 0 deletions server/config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Config
# watchers to your application. For example, we use it
# with brunch.io to recompile .js and .css sources.
config :orcasite, OrcasiteWeb.Endpoint,
url: [host: "localhost"],
http: [port: 4000],
debug_errors: true,
code_reloader: true,
Expand Down
2 changes: 0 additions & 2 deletions server/config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,3 @@ if config_env() == :prod do

config :swoosh, :api_client, Swoosh.ApiClient.Finch
end

config :mailchimp, api_key: System.get_env("ORCASOUND_MAILCHIMP_API_KEY")
3 changes: 2 additions & 1 deletion server/config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import Config
config :orcasite, OrcasiteWeb.Endpoint,
http: [port: 4001],
server: false,
secret_key_base: "99+mOEo5AOlhlQi4sJJd5MWnAdFFzm64"
secret_key_base: "99+mOEo5AOlhlQi4sJJd5MWnAdFFzm64",
check_origin: false

# Print only warnings and errors during test
config :logger, level: :warning
Expand Down
66 changes: 5 additions & 61 deletions server/lib/orcasite/accounts/accounts.ex
Original file line number Diff line number Diff line change
@@ -1,67 +1,11 @@
defmodule Orcasite.Accounts do
import Ecto.Query, warn: false
use Ash.Api, extensions: [AshAdmin.Api]

alias Orcasite.Repo
alias Orcasite.Accounts.User

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

def find_user_by_auth_token(auth_token),
do: User |> where(auth_token: ^auth_token) |> Repo.one()

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

def list_users(params \\ %{pagination: %{page: 1, page_size: 10}}) do
User
|> order_by(desc: :inserted_at)
|> Repo.paginate(page: params.pagination.page, page_size: params.pagination.page_size)
end

def update_user(%User{id: id} = user, attrs, %User{admin: admin, id: current_user_id})
when (is_boolean(admin) and admin) or current_user_id == id do
user
|> User.changeset(attrs)
|> Repo.update()
end

def update_password(user, password) do
user
|> User.password_changeset(%{password: password})
|> Repo.update()
end

def login_user(user) do
{:ok, jwt, _} = OrcasiteWeb.Guardian.encode_and_sign(user)
{:ok, _} = store_token(user, jwt)
end

def store_token(%User{} = user, auth_token) do
user
|> User.store_token_changeset(%{auth_token: auth_token})
|> Repo.update()
end

def revoke_token(%User{} = user) do
user
|> User.store_token_changeset(%{auth_token: nil})
|> Repo.update()
resources do
registry Orcasite.Accounts.Registry
end

def authenticate(%{email: email, password: password}) do
User
|> Repo.get_by(email: String.downcase(email))
|> case do
nil ->
# Take up time
Bcrypt.no_user_verify()
{:error, :wrong_credentials}

user ->
Bcrypt.check_pass(user, password)
end
admin do
show? true
end
end
8 changes: 8 additions & 0 deletions server/lib/orcasite/accounts/registry.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
defmodule Orcasite.Accounts.Registry do
use Ash.Registry, extensions: [Ash.Registry.ResourceValidations]

entries do
entry Orcasite.Accounts.User
entry Orcasite.Accounts.Token
end
end
21 changes: 21 additions & 0 deletions server/lib/orcasite/accounts/token.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
defmodule Orcasite.Accounts.Token do
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication.TokenResource]

token do
api Orcasite.Accounts
end

postgres do
table "tokens"
repo Orcasite.Repo
end

# If using policies, add the following bypass:
# policies do
# bypass AshAuthentication.Checks.AshAuthenticationInteraction do
# authorize_if always()
# end
# end
end
75 changes: 35 additions & 40 deletions server/lib/orcasite/accounts/user.ex
Original file line number Diff line number Diff line change
@@ -1,54 +1,49 @@
defmodule Orcasite.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication]

alias Orcasite.Accounts.User

schema "users" do
field(:email, :string)
field(:password_hash, :string)
field(:first_name, :string)
field(:last_name, :string)
field(:admin, :boolean)
field(:auth_token, :string)
attributes do
uuid_primary_key :id
attribute :email, :ci_string, allow_nil?: false
attribute :hashed_password, :string, allow_nil?: false, sensitive?: true
attribute :first_name, :string
attribute :last_name, :string
attribute :admin, :boolean

field(:password, :string, virtual: true)

timestamps()
end

def changeset(%User{} = user, attrs) do
user
|> cast(attrs, [:email, :first_name, :last_name])
|> validate_required([:email])
|> update_change(:email, &String.downcase/1)
|> validate_format(:email, ~r/^.+@.+$/)
|> unique_constraint(:email, name: :users_lower_email_index)
end

def create_changeset(%User{} = user, attrs) do
user
|> changeset(attrs)
|> password_changeset(attrs)
create_timestamp :inserted_at
update_timestamp :updated_at
end

def password_changeset(user_or_changeset, attrs) do
user_or_changeset
|> cast(attrs, [:password])
|> validate_length(:password, min: 6, max: 100)
|> hash_password
authentication do
api Orcasite.Accounts

strategies do
password :password do
identity_field :email
end
end

tokens do
enabled? true
token_resource Orcasite.Accounts.Token
signing_secret fn _, _ ->
{:ok, Application.get_env(:orcasite, OrcasiteWeb.Endpoint)[:secret_key_base]}
end
end
end

def store_token_changeset(%User{} = user, attrs) do
user
|> cast(attrs, [:auth_token])
postgres do
table "users"
repo Orcasite.Repo
end

defp hash_password(%Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset) do
put_change(changeset, :password_hash, Bcrypt.hash_pwd_salt(password))
identities do
identity :unique_email, [:email]
end

defp hash_password(changeset) do
changeset
actions do
defaults [:read, :create, :update, :destroy]
end
end
4 changes: 2 additions & 2 deletions server/lib/orcasite/notifications/email.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ defmodule Orcasite.Notifications.Email do
assigns
|> Map.put(
:unsubscribe_url,
url(~p"/auth/subscription/unsubscribe?token=#{assigns.unsubscribe_token}")
url(~p"/s/auth/subscription/unsubscribe?token=#{assigns.unsubscribe_token}")
)

"""
Expand Down Expand Up @@ -59,7 +59,7 @@ defmodule Orcasite.Notifications.Email do
assigns
|> Map.put(
:unsubscribe_url,
url(~p"/auth/subscription/unsubscribe?token=#{assigns.unsubscribe_token}")
url(~p"/s/auth/subscription/unsubscribe?token=#{assigns.unsubscribe_token}")
)

"""
Expand Down
2 changes: 1 addition & 1 deletion server/lib/orcasite/notifications/resources/token.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ defmodule Orcasite.Notifications.Token do
end

postgres do
table "tokens"
table "subscriber_tokens"
repo Orcasite.Repo
end

Expand Down
2 changes: 1 addition & 1 deletion server/lib/orcasite_web.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ defmodule OrcasiteWeb do
below. Instead, define additional modules and import
those modules here.
"""
def static_paths, do: ~w(css fonts images js favicon.ico robots.txt)
def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)

def router do
quote do
Expand Down
13 changes: 13 additions & 0 deletions server/lib/orcasite_web/auth_overrides.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
defmodule OrcasiteWeb.AuthOverrides do
use AshAuthentication.Phoenix.Overrides
alias AshAuthentication.Phoenix.Components

override Components.Banner do
set :image_url, "/images/logo.png"
set :image_class, "w-80"
end

override Components.MagicLink do
set :root_class, "hidden"
end
end
4 changes: 2 additions & 2 deletions server/lib/orcasite_web/components/layouts/root.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
<.live_title>
<%= assigns[:page_title] || "Orcasound" %>
</.live_title>
<link rel="stylesheet" href={~p"/css/app.css"}/>
<script defer type="text/javascript" src={~p"/js/app.js"}></script>
<link rel="stylesheet" href={~p"/assets/app.css"}/>
<script defer type="text/javascript" src={~p"/assets/app.js"}></script>
</head>
<body>
<%= @inner_content %>
Expand Down
36 changes: 36 additions & 0 deletions server/lib/orcasite_web/controllers/auth_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
defmodule OrcasiteWeb.AuthController do
use OrcasiteWeb, :controller
use AshAuthentication.Phoenix.Controller

def success(conn, _activity, nil = _user, _token) do
return_to = get_session(conn, :return_to) || ~p"/"

conn
|> clear_session()
|> redirect(to: return_to)
end

def success(conn, _activity, user, _token) do
return_to = get_session(conn, :return_to) || ~p"/"

conn
|> delete_session(:return_to)
|> store_in_session(user)
|> assign(:current_user, user)
|> redirect(to: return_to)
end

def failure(conn, _activity, _reason) do
conn
|> put_status(401)
|> render("failure.html")
end

def sign_out(conn, _params) do
return_to = get_session(conn, :return_to) || ~p"/"

conn
|> clear_session()
|> redirect(to: return_to)
end
end
5 changes: 5 additions & 0 deletions server/lib/orcasite_web/controllers/auth_html.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
defmodule OrcasiteWeb.AuthHTML do
use OrcasiteWeb, :html

embed_templates "auth_html/*"
end
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1 class="text-2xl">Authentication Error</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>Success! You've been signed in</h1>
Loading