mirror of
https://github.com/ash-project/ash_hq.git
synced 2024-09-19 12:53:49 +12:00
improvement: move to ash_authentication (#71)
This commit is contained in:
parent
991b8a73bf
commit
00b3a8e1d6
81 changed files with 1331 additions and 2620 deletions
|
@ -7,7 +7,9 @@
|
|||
:ash_graphql,
|
||||
:surface,
|
||||
:ash_admin,
|
||||
:ash_csv
|
||||
:ash_csv,
|
||||
:ash_authentication,
|
||||
:ash_authentication_phoenix
|
||||
],
|
||||
inputs: [
|
||||
"*.{ex,exs}",
|
||||
|
|
|
@ -2,7 +2,13 @@ const colors = require("tailwindcss/colors");
|
|||
|
||||
module.exports = {
|
||||
mode: "jit",
|
||||
content: ["./js/**/*.js", "../lib/*_web/**/*.*ex", "../../../sunflower_ui/**/*.ex", "../priv/blog/**/*.md"],
|
||||
content: [
|
||||
"./js/**/*.js",
|
||||
"../lib/*_web/**/*.*ex",
|
||||
"../deps/sunflower_ui/**/*.ex",
|
||||
"../priv/blog/**/*.md",
|
||||
"../deps/ash_authentication_phoenix/**/*.ex"
|
||||
],
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
|
|
|
@ -21,7 +21,15 @@ config :ash_hq, AshHq.Repo,
|
|||
config :spark, :formatter,
|
||||
remove_parens?: true,
|
||||
"AshHq.Resource": [
|
||||
type: Ash.Resource
|
||||
type: Ash.Resource,
|
||||
section_order: [
|
||||
:authentication,
|
||||
:token,
|
||||
:attributes,
|
||||
:relationships,
|
||||
:policies,
|
||||
:postgres
|
||||
]
|
||||
],
|
||||
"Ash.Flow": []
|
||||
|
||||
|
|
|
@ -22,6 +22,10 @@ config :git_ops,
|
|||
manage_readme_version: "README.md",
|
||||
version_tag_prefix: "v"
|
||||
|
||||
secret_key_base = "FxKFwVYhDFah3bLLXXqWdpdcLf5e5T1UyVM6XQp7kCt/Reg5yuAEI3upAVDRoP5e"
|
||||
|
||||
config :ash_hq, token_signing_secret: secret_key_base
|
||||
|
||||
# For development, we disable any cache and enable
|
||||
# debugging and code reloading.
|
||||
#
|
||||
|
@ -35,7 +39,7 @@ config :ash_hq, AshHqWeb.Endpoint,
|
|||
check_origin: false,
|
||||
code_reloader: true,
|
||||
debug_errors: true,
|
||||
secret_key_base: "FxKFwVYhDFah3bLLXXqWdpdcLf5e5T1UyVM6XQp7kCt/Reg5yuAEI3upAVDRoP5e",
|
||||
secret_key_base: secret_key_base,
|
||||
watchers: [
|
||||
# Start the esbuild watcher by calling Esbuild.install_and_run(:default, args)
|
||||
esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]},
|
||||
|
|
|
@ -12,6 +12,23 @@ if System.get_env("PHX_SERVER") && System.get_env("RELEASE_NAME") do
|
|||
config :ash_hq, AshHqWeb.Endpoint, server: true
|
||||
end
|
||||
|
||||
config :ash_hq, :github,
|
||||
client_id: System.fetch_env("GITHUB_CLIENT_ID"),
|
||||
client_secret: System.fetch_env("GITHUB_CLIENT_SECRET"),
|
||||
redirect_uri: System.fetch_env("GITHUB_REDIRECT_URI")
|
||||
|
||||
host = System.get_env("PHX_HOST") || "localhost"
|
||||
port = String.to_integer(System.get_env("PORT") || "4000")
|
||||
|
||||
ash_hq_url =
|
||||
case port do
|
||||
443 -> "https://#{host}"
|
||||
80 -> "http://#{host}"
|
||||
port -> "http://#{host}:#{port}"
|
||||
end
|
||||
|
||||
config :ash_hq, url: ash_hq_url
|
||||
|
||||
if config_env() == :prod do
|
||||
database_url =
|
||||
System.get_env("DATABASE_URL") ||
|
||||
|
@ -38,8 +55,8 @@ if config_env() == :prod do
|
|||
You can generate one by calling: mix phx.gen.secret
|
||||
"""
|
||||
|
||||
host = System.get_env("PHX_HOST") || "ash-hq.org"
|
||||
port = String.to_integer(System.get_env("PORT") || "4000")
|
||||
config :ash_hq,
|
||||
token_signing_secret: secret_key_base
|
||||
|
||||
config :ash_hq, AshHqWeb.Endpoint,
|
||||
server: true,
|
||||
|
|
|
@ -13,11 +13,15 @@ config :ash_hq, AshHq.Repo,
|
|||
pool: Ecto.Adapters.SQL.Sandbox,
|
||||
pool_size: 10
|
||||
|
||||
secret_key_base = "766JP3UO+dTbVfydE4RafFKbfUoudccDz1zS7x1N75WSiEJTq6dDR4r04+McH41m"
|
||||
|
||||
config :ash_hq, token_signing_secret: secret_key_base
|
||||
|
||||
# We don't run a server during test. If one is required,
|
||||
# you can enable the server option below.
|
||||
config :ash_hq, AshHqWeb.Endpoint,
|
||||
http: [ip: {127, 0, 0, 1}, port: 4002],
|
||||
secret_key_base: "766JP3UO+dTbVfydE4RafFKbfUoudccDz1zS7x1N75WSiEJTq6dDR4r04+McH41m",
|
||||
secret_key_base: secret_key_base,
|
||||
server: false
|
||||
|
||||
config :ash_hq, cloak_key: "J6ED3yBWjlaOW/5byrukZTEryKa++yXWblJuhP91Qq8="
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
defmodule AshHq.Accounts.EmailNotifier do
|
||||
@moduledoc """
|
||||
Hooks into resource notifications on the user token resource to send emails
|
||||
"""
|
||||
|
||||
def notify(%Ash.Notifier.Notification{
|
||||
resource: AshHq.Accounts.UserToken,
|
||||
action: %{name: :build_email_token},
|
||||
metadata: %{
|
||||
user: user,
|
||||
url: url,
|
||||
confirm?: true
|
||||
}
|
||||
}) do
|
||||
AshHq.Accounts.Emails.deliver_confirmation_instructions(user, url)
|
||||
end
|
||||
|
||||
def notify(%Ash.Notifier.Notification{
|
||||
resource: AshHq.Accounts.UserToken,
|
||||
action: %{name: :build_email_token},
|
||||
metadata: %{
|
||||
user: user,
|
||||
url: url,
|
||||
update?: true
|
||||
}
|
||||
}) do
|
||||
AshHq.Accounts.Emails.deliver_update_email_instructions(user, url)
|
||||
end
|
||||
|
||||
def notify(%Ash.Notifier.Notification{
|
||||
resource: AshHq.Accounts.UserToken,
|
||||
action: %{name: :build_email_token},
|
||||
metadata: %{
|
||||
user: user,
|
||||
url: url,
|
||||
reset?: true
|
||||
}
|
||||
}) do
|
||||
AshHq.Accounts.Emails.deliver_reset_password_instructions(user, url)
|
||||
end
|
||||
|
||||
def notify(_other) do
|
||||
:ok
|
||||
end
|
||||
end
|
|
@ -1,16 +0,0 @@
|
|||
defmodule AshHq.Accounts.Preparations.DetermineDaysForToken do
|
||||
@moduledoc """
|
||||
Sets a `days_for_token` context on the query.
|
||||
|
||||
This corresponds to how many days the token should be considered valid. See `AshHq.Accounts.User.Helpers` for more.
|
||||
"""
|
||||
use Ash.Resource.Preparation
|
||||
|
||||
def prepare(query, _opts, _) do
|
||||
Ash.Query.put_context(
|
||||
query,
|
||||
:days_for_token,
|
||||
AshHq.Accounts.User.Helpers.days_for_token(Ash.Query.get_argument(query, :context))
|
||||
)
|
||||
end
|
||||
end
|
|
@ -1,23 +0,0 @@
|
|||
defmodule AshHq.Accounts.Preparations.SetHashedToken do
|
||||
@moduledoc """
|
||||
Takes a provided token and hashes it, setting it as the context `hashed_token`
|
||||
"""
|
||||
|
||||
use Ash.Resource.Preparation
|
||||
|
||||
@hash_algorithm :sha256
|
||||
|
||||
def prepare(query, _opts, _) do
|
||||
case Ash.Query.get_argument(query, :token) do
|
||||
nil ->
|
||||
query
|
||||
|
||||
token ->
|
||||
Ash.Query.put_context(
|
||||
query,
|
||||
:hashed_token,
|
||||
:crypto.hash(@hash_algorithm, token)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,58 +0,0 @@
|
|||
defmodule AshHq.Accounts.User.Changes.CreateEmailConfirmationToken do
|
||||
@moduledoc "A change that triggers an email token build and an email notification"
|
||||
|
||||
use Ash.Resource.Change
|
||||
|
||||
def change(changeset, opts, _context) do
|
||||
if opts[:on_argument] && Ash.Changeset.get_argument(changeset, opts[:on_argument]) do
|
||||
Ash.Changeset.after_action(changeset, fn changeset, user ->
|
||||
AshHq.Accounts.UserToken
|
||||
|> Ash.Changeset.for_create(
|
||||
:build_email_token,
|
||||
%{
|
||||
email: user.email,
|
||||
context: "confirm",
|
||||
sent_to: user.email,
|
||||
user: user.id
|
||||
},
|
||||
authorize?: false
|
||||
)
|
||||
|> AshHq.Accounts.create(return_notifications?: true)
|
||||
|> case do
|
||||
{:ok, email_token, notifications} ->
|
||||
{:ok,
|
||||
%{
|
||||
user
|
||||
| __metadata__:
|
||||
Map.put(user.__metadata__, :token, email_token.__metadata__.url_token)
|
||||
}, Enum.map(notifications, &set_metadata(&1, user, changeset, email_token))}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end)
|
||||
else
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
defp set_metadata(notification, user, changeset, email_token) do
|
||||
url =
|
||||
case Ash.Changeset.get_argument(changeset, :confirmation_url_fun) do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
fun ->
|
||||
fun.(email_token.__metadata__.url_token)
|
||||
end
|
||||
|
||||
%{
|
||||
notification
|
||||
| metadata: %{
|
||||
user: user,
|
||||
url: url,
|
||||
confirm?: true
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
|
@ -1,54 +0,0 @@
|
|||
defmodule AshHq.Accounts.User.Changes.CreateEmailUpdateToken do
|
||||
@moduledoc "A change that triggers an email token build and an email notification"
|
||||
|
||||
use Ash.Resource.Change
|
||||
|
||||
def change(original_changeset, _opts, _context) do
|
||||
Ash.Changeset.after_action(original_changeset, fn changeset, user ->
|
||||
AshHq.Accounts.UserToken
|
||||
|> Ash.Changeset.for_create(
|
||||
:build_email_token,
|
||||
%{
|
||||
email: user.email,
|
||||
context: "change:#{user.email}",
|
||||
sent_to: original_changeset.attributes[:email],
|
||||
user: user.id
|
||||
},
|
||||
authorize?: false
|
||||
)
|
||||
|> AshHq.Accounts.create(return_notifications?: true)
|
||||
|> case do
|
||||
{:ok, email_token, notifications} ->
|
||||
{:ok,
|
||||
%{
|
||||
user
|
||||
| __metadata__:
|
||||
Map.put(user.__metadata__, :token, email_token.__metadata__.url_token)
|
||||
}, Enum.map(notifications, &set_metadata(&1, user, changeset, email_token))}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp set_metadata(notification, user, changeset, email_token) do
|
||||
url =
|
||||
case Ash.Changeset.get_argument(changeset, :update_url_fun) do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
fun ->
|
||||
fun.(email_token.__metadata__.url_token)
|
||||
end
|
||||
|
||||
%{
|
||||
notification
|
||||
| metadata: %{
|
||||
user: user,
|
||||
url: url,
|
||||
update?: true
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
|
@ -1,49 +0,0 @@
|
|||
defmodule AshHq.Accounts.User.Changes.CreateResetPasswordToken do
|
||||
@moduledoc "A change that triggers an reset password token build and an email notification"
|
||||
|
||||
use Ash.Resource.Change
|
||||
|
||||
def change(changeset, _opts, _context) do
|
||||
Ash.Changeset.after_action(changeset, fn changeset, user ->
|
||||
AshHq.Accounts.UserToken
|
||||
|> Ash.Changeset.for_create(
|
||||
:build_email_token,
|
||||
%{email: user.email, context: "reset_password", sent_to: user.email, user: user.id},
|
||||
authorize?: false
|
||||
)
|
||||
|> AshHq.Accounts.create(return_notifications?: true)
|
||||
|> case do
|
||||
{:ok, email_token, notifications} ->
|
||||
{:ok,
|
||||
%{
|
||||
user
|
||||
| __metadata__:
|
||||
Map.put(user.__metadata__, :token, email_token.__metadata__.url_token)
|
||||
}, Enum.map(notifications, &set_metadata(&1, user, changeset, email_token))}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp set_metadata(notification, user, changeset, email_token) do
|
||||
url =
|
||||
case Ash.Changeset.get_argument(changeset, :reset_password_url_fun) do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
fun ->
|
||||
fun.(email_token.__metadata__.url_token)
|
||||
end
|
||||
|
||||
%{
|
||||
notification
|
||||
| metadata: %{
|
||||
user: user,
|
||||
url: url,
|
||||
reset?: true
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
|
@ -1,27 +0,0 @@
|
|||
defmodule AshHq.Accounts.User.Changes.DeleteConfirmTokens do
|
||||
@moduledoc "A change that deletes all confirm tokens for a user, if the `delete_confirm_tokens` argument is present"
|
||||
use Ash.Resource.Change
|
||||
require Ash.Query
|
||||
|
||||
def change(changeset, _opts, _context) do
|
||||
if Ash.Changeset.get_argument(changeset, :delete_confirm_tokens) do
|
||||
Ash.Changeset.after_action(changeset, fn _changeset, user ->
|
||||
days = AshHq.Accounts.User.Helpers.days_for_token("confirm")
|
||||
|
||||
{:ok, query} =
|
||||
AshHq.Accounts.UserToken
|
||||
|> Ash.Query.filter(
|
||||
created_at > ago(^days, :day) and context == "confirm" and
|
||||
sent_to == user.email
|
||||
)
|
||||
|> Ash.Query.data_layer_query()
|
||||
|
||||
AshHq.Repo.delete_all(query)
|
||||
|
||||
{:ok, user}
|
||||
end)
|
||||
else
|
||||
changeset
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,23 +0,0 @@
|
|||
defmodule AshHq.Accounts.User.Changes.DeleteEmailChangeTokens do
|
||||
@moduledoc "A change that deletes all email change tokens for a user"
|
||||
use Ash.Resource.Change
|
||||
require Ash.Query
|
||||
|
||||
def change(original_changeset, _opts, _context) do
|
||||
Ash.Changeset.after_action(original_changeset, fn changeset, user ->
|
||||
email = original_changeset.data.email
|
||||
context = "change:#{email}"
|
||||
|
||||
token = Ash.Changeset.get_argument(changeset, :token)
|
||||
|
||||
{:ok, query} =
|
||||
AshHq.Accounts.UserToken
|
||||
|> Ash.Query.filter(token == ^token and context == ^context)
|
||||
|> Ash.Query.data_layer_query()
|
||||
|
||||
AshHq.Repo.delete_all(query)
|
||||
|
||||
{:ok, user}
|
||||
end)
|
||||
end
|
||||
end
|
|
@ -1,34 +0,0 @@
|
|||
defmodule AshHq.Accounts.User.Changes.GetEmailFromToken do
|
||||
@moduledoc "A change that fetches the token for an email change"
|
||||
|
||||
use Ash.Resource.Change
|
||||
|
||||
def change(changeset, _opts, _) do
|
||||
changeset
|
||||
|> Ash.Changeset.before_action(fn changeset ->
|
||||
if changeset.valid? do
|
||||
token = Ash.Changeset.get_argument(changeset, :token)
|
||||
|
||||
AshHq.Accounts.UserToken
|
||||
|> Ash.Query.for_read(
|
||||
:verify_email_token,
|
||||
%{token: token, context: "change:#{changeset.data.email}"},
|
||||
authorize?: false
|
||||
)
|
||||
|> AshHq.Accounts.read_one()
|
||||
|> case do
|
||||
{:ok, %{sent_to: new_email}} ->
|
||||
Ash.Changeset.change_attribute(changeset, :email, new_email)
|
||||
|
||||
_ ->
|
||||
Ash.Changeset.add_error(changeset,
|
||||
field: :token,
|
||||
message: "Could not find corresponding token"
|
||||
)
|
||||
end
|
||||
else
|
||||
changeset
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
|
@ -1,19 +0,0 @@
|
|||
defmodule AshHq.Accounts.User.Changes.HashPassword do
|
||||
@moduledoc "A change that hashes the `password` attribute for valid changes"
|
||||
|
||||
use Ash.Resource.Change
|
||||
|
||||
alias Ash.Changeset
|
||||
|
||||
def change(changeset, _opts, _) do
|
||||
Changeset.before_action(changeset, fn changeset ->
|
||||
case Changeset.get_argument(changeset, :password) do
|
||||
nil ->
|
||||
changeset
|
||||
|
||||
value ->
|
||||
Changeset.change_attribute(changeset, :hashed_password, Bcrypt.hash_pwd_salt(value))
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
|
@ -8,7 +8,9 @@ defmodule AshHq.Accounts.User.Changes.RemoveAllTokens do
|
|||
require Ash.Query
|
||||
|
||||
def change(changeset, _opts, _context) do
|
||||
Ash.Changeset.after_action(changeset, fn _changeset, user ->
|
||||
Ash.Changeset.after_action(
|
||||
changeset,
|
||||
fn _changeset, user ->
|
||||
{:ok, query} =
|
||||
AshHq.Accounts.UserToken
|
||||
|> Ash.Query.filter(user_id == ^user.id)
|
||||
|
@ -17,6 +19,8 @@ defmodule AshHq.Accounts.User.Changes.RemoveAllTokens do
|
|||
AshHq.Repo.delete_all(query)
|
||||
|
||||
{:ok, user}
|
||||
end)
|
||||
end,
|
||||
prepend?: true
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
defmodule AshHq.Accounts.User.Helpers do
|
||||
@moduledoc "Contains values used in various places for authentication"
|
||||
@reset_password_validity_in_days 1
|
||||
@confirm_validity_in_days 7
|
||||
@change_email_validity_in_days 7
|
||||
@session_validity_in_days 60
|
||||
|
||||
def days_for_token("reset_password"), do: @reset_password_validity_in_days
|
||||
def days_for_token("confirm"), do: @confirm_validity_in_days
|
||||
def days_for_token("session"), do: @session_validity_in_days
|
||||
def days_for_token("change:" <> _), do: @change_email_validity_in_days
|
||||
|
||||
def valid_password?(%AshHq.Accounts.User{hashed_password: hashed_password}, password)
|
||||
when is_binary(hashed_password) and byte_size(password) > 0 do
|
||||
Bcrypt.verify_pass(password, hashed_password)
|
||||
end
|
||||
|
||||
def valid_password?(_, _) do
|
||||
Bcrypt.no_user_verify()
|
||||
|
||||
false
|
||||
end
|
||||
end
|
|
@ -1,26 +0,0 @@
|
|||
defmodule AshHq.Accounts.User.Preparations.ValidatePassword do
|
||||
@moduledoc """
|
||||
Given the result of a query for users, and a password argument, ensures that the `password` is valid.
|
||||
|
||||
If there is more or less than one result, or if the password is invalid, then this removes the results of the query.
|
||||
In this way, you can't tell from the outside whether or not the password was invalid or there was no matching account.
|
||||
"""
|
||||
use Ash.Resource.Preparation
|
||||
|
||||
def prepare(query, _opts, _) do
|
||||
Ash.Query.after_action(query, fn
|
||||
query, [result] ->
|
||||
password = Ash.Query.get_argument(query, :password)
|
||||
|
||||
if AshHq.Accounts.User.Helpers.valid_password?(result, password) do
|
||||
{:ok, [result]}
|
||||
else
|
||||
{:ok, []}
|
||||
end
|
||||
|
||||
_, _ ->
|
||||
Bcrypt.no_user_verify()
|
||||
{:ok, []}
|
||||
end)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,21 @@
|
|||
defmodule AshHq.Accounts.User.Senders.SendConfirmationEmail do
|
||||
@moduledoc """
|
||||
Sends a confirmation email for initial sign up or email change.
|
||||
"""
|
||||
use AshAuthentication.Sender
|
||||
use AshHqWeb, :verified_routes
|
||||
|
||||
def send(user, token, opts) do
|
||||
if opts[:changeset] && opts[:changeset].action.name == :update_email do
|
||||
AshHq.Accounts.Emails.deliver_update_email_instructions(
|
||||
%{user | email: Ash.Changeset.get_attribute(opts[:changeset], :email)},
|
||||
AshHqWeb.Routes.url(~p"/auth/user/confirm?confirm=#{token}")
|
||||
)
|
||||
else
|
||||
AshHq.Accounts.Emails.deliver_confirmation_instructions(
|
||||
user,
|
||||
AshHqWeb.Routes.url(~p"/auth/user/confirm?confirm=#{token}")
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,14 @@
|
|||
defmodule AshHq.Accounts.User.Senders.SendPasswordResetEmail do
|
||||
@moduledoc """
|
||||
Sends a password reset email
|
||||
"""
|
||||
use AshAuthentication.Sender
|
||||
use AshHqWeb, :verified_routes
|
||||
|
||||
def send(user, token, _) do
|
||||
AshHq.Accounts.Emails.deliver_reset_password_instructions(
|
||||
user,
|
||||
~p"/password-reset/#{token}"
|
||||
)
|
||||
end
|
||||
end
|
|
@ -3,51 +3,47 @@ defmodule AshHq.Accounts.User do
|
|||
|
||||
use AshHq.Resource,
|
||||
data_layer: AshPostgres.DataLayer,
|
||||
authorizers: [Ash.Policy.Authorizer]
|
||||
authorizers: [Ash.Policy.Authorizer],
|
||||
extensions: [AshAuthentication]
|
||||
|
||||
alias AshHq.Accounts.Preparations, warn: false
|
||||
import Ash.Changeset
|
||||
|
||||
postgres do
|
||||
table "users"
|
||||
repo AshHq.Repo
|
||||
require Ash.Query
|
||||
|
||||
authentication do
|
||||
api AshHq.Accounts
|
||||
|
||||
strategies do
|
||||
password :password do
|
||||
identity_field :email
|
||||
hashed_password_field :hashed_password
|
||||
|
||||
resettable do
|
||||
sender AshHq.Accounts.User.Senders.SendPasswordResetEmail
|
||||
end
|
||||
end
|
||||
|
||||
policies do
|
||||
policy action(:read) do
|
||||
authorize_if expr(id == ^actor(:id))
|
||||
github do
|
||||
client_id AshHq.Accounts.Secrets
|
||||
client_secret AshHq.Accounts.Secrets
|
||||
redirect_uri AshHq.Accounts.Secrets
|
||||
end
|
||||
end
|
||||
|
||||
policy action(:change_password) do
|
||||
description "Allow the user to change their own password"
|
||||
authorize_if expr(id == ^actor(:id))
|
||||
tokens do
|
||||
enabled? true
|
||||
token_resource AshHq.Accounts.UserToken
|
||||
signing_secret AshHq.Accounts.Secrets
|
||||
store_all_tokens? true
|
||||
require_token_presence_for_authentication? true
|
||||
end
|
||||
|
||||
policy action(:deliver_update_email_instructions) do
|
||||
description "Allow a user to request an update their own email"
|
||||
authorize_if expr(id == ^actor(:id))
|
||||
end
|
||||
add_ons do
|
||||
confirmation :confirm do
|
||||
monitor_fields [:email]
|
||||
|
||||
policy action(:by_email_and_password) do
|
||||
description "Allow looking up by email/password combo (logging in) for unauthenticated users only."
|
||||
forbid_if actor_present()
|
||||
authorize_if always()
|
||||
sender AshHq.Accounts.User.Senders.SendConfirmationEmail
|
||||
end
|
||||
|
||||
policy action(:deliver_user_confirmation_instructions) do
|
||||
description "A logged in user can request email confirmation for themselves to be sent again"
|
||||
authorize_if expr(id == ^actor(:id))
|
||||
end
|
||||
|
||||
policy action(:update_merch_settings) do
|
||||
description "A logged in user can update their merch settings"
|
||||
authorize_if expr(id == ^actor(:id))
|
||||
end
|
||||
|
||||
policy action(:register) do
|
||||
description "Allow looking up by email/password combo (logging in) for unauthenticated users only."
|
||||
forbid_if actor_present()
|
||||
authorize_if always()
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -60,9 +56,7 @@ defmodule AshHq.Accounts.User do
|
|||
max_length: 160
|
||||
]
|
||||
|
||||
attribute :confirmed_at, :utc_datetime_usec
|
||||
|
||||
attribute :hashed_password, :string, private?: true
|
||||
attribute :hashed_password, :string, private?: true, sensitive?: true
|
||||
|
||||
attribute :encrypted_name, AshHq.Types.EncryptedString
|
||||
attribute :encrypted_address, AshHq.Types.EncryptedString
|
||||
|
@ -79,111 +73,150 @@ defmodule AshHq.Accounts.User do
|
|||
end
|
||||
end
|
||||
|
||||
policies do
|
||||
bypass AshAuthentication.Checks.AshAuthenticationInteraction do
|
||||
authorize_if always()
|
||||
end
|
||||
|
||||
policy action(:read) do
|
||||
authorize_if expr(id == ^actor(:id))
|
||||
end
|
||||
|
||||
policy action(:update_email) do
|
||||
description "A logged in user can update their email"
|
||||
authorize_if expr(id == ^actor(:id))
|
||||
end
|
||||
|
||||
policy action(:resend_confirmation_instructions) do
|
||||
description "A logged in user can request an email confirmation"
|
||||
authorize_if expr(id == ^actor(:id))
|
||||
end
|
||||
|
||||
policy action(:change_password) do
|
||||
description "A logged in user can reset their password"
|
||||
authorize_if expr(id == ^actor(:id))
|
||||
end
|
||||
|
||||
policy action(:update_merch_settings) do
|
||||
description "A logged in user can update their merch settings"
|
||||
authorize_if expr(id == ^actor(:id))
|
||||
end
|
||||
end
|
||||
|
||||
postgres do
|
||||
table "users"
|
||||
repo AshHq.Repo
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:read]
|
||||
|
||||
read :by_email_and_password do
|
||||
argument :email, :string, allow_nil?: false, sensitive?: true
|
||||
argument :password, :string, allow_nil?: false, sensitive?: true
|
||||
|
||||
prepare AshHq.Accounts.User.Preparations.ValidatePassword
|
||||
|
||||
filter expr(email == ^arg(:email))
|
||||
create :register_with_github do
|
||||
argument :user_info, :map do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
read :by_token do
|
||||
argument :token, :url_encoded_binary, allow_nil?: false
|
||||
argument :context, :string, allow_nil?: false
|
||||
prepare Preparations.DetermineDaysForToken
|
||||
|
||||
filter expr(
|
||||
token.token == ^arg(:token) and token.context == ^arg(:context) and
|
||||
token.created_at > ago(^context(:days_for_token), :day)
|
||||
)
|
||||
argument :oauth_tokens, :map do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
read :with_verified_email_token do
|
||||
argument :token, :url_encoded_binary, allow_nil?: false
|
||||
argument :context, :string, allow_nil?: false
|
||||
change fn changeset, _ ->
|
||||
user_info = Ash.Changeset.get_argument(changeset, :user_info)
|
||||
|
||||
prepare AshHq.Accounts.Preparations.SetHashedToken
|
||||
prepare AshHq.Accounts.Preparations.DetermineDaysForToken
|
||||
|
||||
filter expr(
|
||||
token.created_at > ago(^context(:days_for_token), :day) and
|
||||
token.token == ^context(:hashed_token) and token.context == ^arg(:context) and
|
||||
token.sent_to == email
|
||||
)
|
||||
changeset =
|
||||
if user_info["email_verified"] do
|
||||
Ash.Changeset.change_new_attribute_lazy(changeset, :confirmed_at, fn ->
|
||||
DateTime.utc_now()
|
||||
end)
|
||||
else
|
||||
changeset
|
||||
end
|
||||
|
||||
create :register do
|
||||
Ash.Changeset.change_attributes(changeset, Map.take(user_info, ["email"]))
|
||||
end
|
||||
|
||||
change AshAuthentication.GenerateTokenChange
|
||||
upsert? true
|
||||
upsert_identity :unique_email
|
||||
end
|
||||
|
||||
update :change_password do
|
||||
accept []
|
||||
|
||||
argument :current_password, :string do
|
||||
sensitive? true
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
argument :password, :string do
|
||||
sensitive? true
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
argument :password_confirmation, :string do
|
||||
sensitive? true
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
validate confirm(:password, :password_confirmation)
|
||||
|
||||
validate {AshHq.Accounts.User.Validations.ValidateCurrentPassword,
|
||||
argument: :current_password} do
|
||||
only_when_valid? true
|
||||
before_action? true
|
||||
end
|
||||
|
||||
change set_context(%{strategy_name: :password})
|
||||
change AshAuthentication.Strategy.Password.HashPasswordChange
|
||||
end
|
||||
|
||||
update :update_email do
|
||||
accept [:email]
|
||||
|
||||
argument :password, :string,
|
||||
allow_nil?: false,
|
||||
constraints: [
|
||||
min_length: 6
|
||||
]
|
||||
|
||||
argument :confirm, :boolean, default: true
|
||||
|
||||
argument :confirmation_url_fun, :function do
|
||||
constraints arity: 1
|
||||
argument :current_password, :string do
|
||||
sensitive? true
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
change AshHq.Accounts.User.Changes.HashPassword
|
||||
change {AshHq.Accounts.User.Changes.CreateEmailConfirmationToken, on_argument: :confirm}
|
||||
validate {AshHq.Accounts.User.Validations.ValidateCurrentPassword,
|
||||
argument: :current_password} do
|
||||
only_when_valid? true
|
||||
before_action? true
|
||||
end
|
||||
end
|
||||
|
||||
update :deliver_user_confirmation_instructions do
|
||||
update :resend_confirmation_instructions do
|
||||
accept []
|
||||
|
||||
argument :confirmation_url_fun, :function do
|
||||
constraints arity: 1
|
||||
change fn changeset, _context ->
|
||||
Ash.Changeset.before_action(changeset, fn changeset ->
|
||||
case AshHq.Accounts.UserToken.email_token_for_user(changeset.data.id,
|
||||
authorize?: false
|
||||
) do
|
||||
{:ok, %{extra_data: %{"email" => changing_to}}} ->
|
||||
temp_changeset = %{
|
||||
changeset
|
||||
| attributes: Map.put(changeset.attributes, :email, changing_to)
|
||||
}
|
||||
|
||||
strategy = AshAuthentication.Info.strategy!(changeset.resource, :confirm)
|
||||
|
||||
{:ok, token} =
|
||||
AshAuthentication.AddOn.Confirmation.confirmation_token(
|
||||
strategy,
|
||||
temp_changeset,
|
||||
changeset.data
|
||||
)
|
||||
|
||||
AshHq.Accounts.User.Senders.SendConfirmationEmail.send(changeset.data, token, [])
|
||||
|
||||
changeset
|
||||
|
||||
_ ->
|
||||
Ash.Changeset.add_error(changeset, "Could not determine what email to use")
|
||||
end
|
||||
|
||||
validate attribute_equals(:confirmed_at, nil), message: "already confirmed"
|
||||
change AshHq.Accounts.User.Changes.CreateEmailConfirmationToken
|
||||
end)
|
||||
end
|
||||
|
||||
update :deliver_update_email_instructions do
|
||||
accept [:email]
|
||||
|
||||
argument :current_password, :string, allow_nil?: false
|
||||
|
||||
argument :update_url_fun, :function do
|
||||
constraints arity: 1
|
||||
end
|
||||
|
||||
validate AshHq.Accounts.User.Validations.ValidateCurrentPassword
|
||||
validate changing(:email)
|
||||
|
||||
change prevent_change(:email)
|
||||
change AshHq.Accounts.User.Changes.CreateEmailUpdateToken
|
||||
end
|
||||
|
||||
update :deliver_user_reset_password_instructions do
|
||||
accept []
|
||||
|
||||
argument :reset_password_url_fun, :function do
|
||||
constraints arity: 1
|
||||
end
|
||||
|
||||
change AshHq.Accounts.User.Changes.CreateResetPasswordToken
|
||||
end
|
||||
|
||||
update :logout do
|
||||
accept []
|
||||
|
||||
change AshHq.Accounts.User.Changes.RemoveAllTokens
|
||||
end
|
||||
|
||||
update :change_email do
|
||||
accept []
|
||||
argument :token, :url_encoded_binary
|
||||
|
||||
change AshHq.Accounts.User.Changes.GetEmailFromToken
|
||||
change AshHq.Accounts.User.Changes.DeleteEmailChangeTokens
|
||||
end
|
||||
|
||||
update :update_merch_settings do
|
||||
|
@ -194,58 +227,12 @@ defmodule AshHq.Accounts.User do
|
|||
change set_attribute(:encrypted_address, arg(:address))
|
||||
change set_attribute(:encrypted_name, arg(:name))
|
||||
end
|
||||
|
||||
update :change_password do
|
||||
accept []
|
||||
|
||||
argument :password, :string,
|
||||
allow_nil?: false,
|
||||
constraints: [
|
||||
min_length: 6
|
||||
]
|
||||
|
||||
argument :password_confirmation, :string, allow_nil?: false
|
||||
argument :current_password, :string
|
||||
|
||||
validate fn changeset ->
|
||||
if get_argument(changeset, :password) ==
|
||||
get_argument(changeset, :password_confirmation) do
|
||||
:ok
|
||||
else
|
||||
{:error, field: :password, message: "password does not match"}
|
||||
end
|
||||
end
|
||||
|
||||
validate AshHq.Accounts.User.Validations.ValidateCurrentPassword
|
||||
|
||||
change AshHq.Accounts.User.Changes.HashPassword
|
||||
change AshHq.Accounts.User.Changes.RemoveAllTokens
|
||||
end
|
||||
|
||||
update :reset_password do
|
||||
accept []
|
||||
|
||||
argument :password, :string,
|
||||
allow_nil?: false,
|
||||
constraints: [
|
||||
min_length: 6
|
||||
]
|
||||
|
||||
argument :password_confirmation, :string, allow_nil?: false
|
||||
|
||||
validate confirm(:password, :password_confirmation)
|
||||
|
||||
change AshHq.Accounts.User.Changes.HashPassword
|
||||
change AshHq.Accounts.User.Changes.RemoveAllTokens
|
||||
end
|
||||
|
||||
update :confirm do
|
||||
accept []
|
||||
argument :delete_confirm_tokens, :boolean, default: false
|
||||
|
||||
change set_attribute(:confirmed_at, &DateTime.utc_now/0)
|
||||
change AshHq.Accounts.User.Changes.DeleteConfirmTokens
|
||||
end
|
||||
code_interface do
|
||||
define_for AshHq.Accounts
|
||||
define :resend_confirmation_instructions
|
||||
define :register_with_password, args: [:email, :password, :password_confirmation]
|
||||
end
|
||||
|
||||
resource do
|
||||
|
@ -255,7 +242,14 @@ defmodule AshHq.Accounts.User do
|
|||
end
|
||||
|
||||
identities do
|
||||
identity :unique_email, [:email]
|
||||
identity :unique_email, [:email] do
|
||||
eager_check_with AshHq.Accounts
|
||||
end
|
||||
end
|
||||
|
||||
changes do
|
||||
change AshHq.Accounts.User.Changes.RemoveAllTokens,
|
||||
where: [action_is(:password_reset_with_password)]
|
||||
end
|
||||
|
||||
validations do
|
||||
|
|
|
@ -8,13 +8,15 @@ defmodule AshHq.Accounts.User.Validations.ValidateCurrentPassword do
|
|||
use Ash.Resource.Validation
|
||||
|
||||
@impl true
|
||||
def validate(changeset, _) do
|
||||
password = Ash.Changeset.get_argument(changeset, :current_password)
|
||||
def validate(changeset, opts) do
|
||||
strategy = AshAuthentication.Info.strategy!(changeset.resource, :password)
|
||||
plaintext_password = Ash.Changeset.get_argument(changeset, opts[:argument])
|
||||
hashed_password = Map.get(changeset.data, strategy.hashed_password_field)
|
||||
|
||||
if AshHq.Accounts.User.Helpers.valid_password?(changeset.data, password) do
|
||||
if strategy.hash_provider.valid?(plaintext_password, hashed_password) do
|
||||
:ok
|
||||
else
|
||||
{:error, [field: :password, message: "is incorrect"]}
|
||||
{:error, [field: opts[:argument], message: "is incorrect"]}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
defmodule AshHq.Accounts.UserToken.Changes.BuildHashedToken do
|
||||
@moduledoc "A change that sets the session token based on the user id"
|
||||
|
||||
use Ash.Resource.Change
|
||||
|
||||
@rand_size 32
|
||||
@hash_algorithm :sha256
|
||||
|
||||
def change(changeset, _opts, _context) do
|
||||
token = :crypto.strong_rand_bytes(@rand_size)
|
||||
|
||||
hashed_token = :crypto.hash(@hash_algorithm, token)
|
||||
|
||||
changeset
|
||||
|> Ash.Changeset.change_attribute(:token, hashed_token)
|
||||
|> Ash.Changeset.after_action(fn _changeset, result ->
|
||||
metadata =
|
||||
Map.put(result.__metadata__, :url_token, Base.url_encode64(token, padding: false))
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
result
|
||||
| __metadata__: metadata
|
||||
}}
|
||||
end)
|
||||
end
|
||||
end
|
|
@ -1,23 +0,0 @@
|
|||
defmodule AshHq.Accounts.UserToken.Changes.BuildSessionToken do
|
||||
@moduledoc "A change that sets the session token based on the user id"
|
||||
|
||||
use Ash.Resource.Change
|
||||
@rand_size 32
|
||||
|
||||
def change(changeset, _opts, _context) do
|
||||
token = :crypto.strong_rand_bytes(@rand_size)
|
||||
|
||||
changeset
|
||||
|> Ash.Changeset.change_attribute(:token, token)
|
||||
|> Ash.Changeset.after_action(fn _changeset, result ->
|
||||
metadata =
|
||||
Map.put(result.__metadata__, :url_token, Base.url_encode64(token, padding: false))
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
result
|
||||
| __metadata__: metadata
|
||||
}}
|
||||
end)
|
||||
end
|
||||
end
|
|
@ -3,19 +3,22 @@ defmodule AshHq.Accounts.UserToken do
|
|||
|
||||
use AshHq.Resource,
|
||||
data_layer: AshPostgres.DataLayer,
|
||||
notifiers: [AshHq.Accounts.EmailNotifier],
|
||||
authorizers: [Ash.Policy.Authorizer]
|
||||
authorizers: [Ash.Policy.Authorizer],
|
||||
extensions: [AshAuthentication.TokenResource]
|
||||
|
||||
postgres do
|
||||
table "user_tokens"
|
||||
repo AshHq.Repo
|
||||
|
||||
references do
|
||||
reference :user, on_delete: :delete, on_update: :update
|
||||
token do
|
||||
api AshHq.Accounts
|
||||
end
|
||||
|
||||
relationships do
|
||||
belongs_to :user, AshHq.Accounts.User
|
||||
end
|
||||
|
||||
policies do
|
||||
bypass AshAuthentication.Checks.AshAuthenticationInteraction do
|
||||
authorize_if always()
|
||||
end
|
||||
|
||||
policy always() do
|
||||
description """
|
||||
There are currently no usages of user tokens resource that should be publicly
|
||||
|
@ -26,57 +29,35 @@ defmodule AshHq.Accounts.UserToken do
|
|||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
postgres do
|
||||
table "user_tokens"
|
||||
repo AshHq.Repo
|
||||
|
||||
attribute :token, :binary
|
||||
attribute :context, :string
|
||||
attribute :sent_to, :string
|
||||
|
||||
create_timestamp :created_at
|
||||
references do
|
||||
reference :user, on_delete: :delete, on_update: :update
|
||||
end
|
||||
|
||||
relationships do
|
||||
belongs_to :user, AshHq.Accounts.User
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:read]
|
||||
defaults [:read, :destroy]
|
||||
|
||||
read :verify_email_token do
|
||||
argument :token, :url_encoded_binary, allow_nil?: false
|
||||
argument :context, :string, allow_nil?: false
|
||||
prepare AshHq.Accounts.Preparations.SetHashedToken
|
||||
prepare AshHq.Accounts.Preparations.DetermineDaysForToken
|
||||
read :email_token_for_user do
|
||||
get? true
|
||||
|
||||
filter expr(
|
||||
token == ^context(:hashed_token) and context == ^arg(:context) and
|
||||
created_at > ago(^context(:days_for_token), :day)
|
||||
)
|
||||
argument :user_id, :uuid do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
create :build_session_token do
|
||||
primary? true
|
||||
prepare build(sort: [updated_at: :desc], limit: 1)
|
||||
|
||||
argument :user, :uuid
|
||||
|
||||
change manage_relationship(:user, type: :append_and_remove)
|
||||
|
||||
change fn changeset, _ ->
|
||||
Ash.Changeset.change_attribute(changeset, :context, "session")
|
||||
filter expr(purpose == "confirm" and not is_nil(extra_data[:email]))
|
||||
end
|
||||
end
|
||||
|
||||
change AshHq.Accounts.UserToken.Changes.BuildSessionToken
|
||||
end
|
||||
|
||||
create :build_email_token do
|
||||
accept [:sent_to, :context]
|
||||
|
||||
argument :user, :uuid
|
||||
|
||||
change manage_relationship(:user, type: :append_and_remove)
|
||||
change AshHq.Accounts.UserToken.Changes.BuildHashedToken
|
||||
end
|
||||
code_interface do
|
||||
define_for AshHq.Accounts
|
||||
define :destroy
|
||||
define :email_token_for_user, args: [:user_id]
|
||||
end
|
||||
|
||||
resource do
|
||||
|
@ -88,4 +69,19 @@ defmodule AshHq.Accounts.UserToken do
|
|||
identities do
|
||||
identity :token_context, [:context, :token]
|
||||
end
|
||||
|
||||
changes do
|
||||
change fn changeset, _ ->
|
||||
case changeset.context[:ash_authentication][:user] do
|
||||
nil ->
|
||||
changeset
|
||||
|
||||
user ->
|
||||
Ash.Changeset.manage_relationship(changeset, :user, user,
|
||||
type: :append_and_remove
|
||||
)
|
||||
end
|
||||
end,
|
||||
on: [:create]
|
||||
end
|
||||
end
|
||||
|
|
17
lib/ash_hq/accounts/secrets.ex
Normal file
17
lib/ash_hq/accounts/secrets.ex
Normal file
|
@ -0,0 +1,17 @@
|
|||
defmodule AshHq.Accounts.Secrets do
|
||||
@moduledoc "Secrets adapter for AshHq authentication"
|
||||
use AshAuthentication.Secret
|
||||
|
||||
@github_secret_keys ~w(client_id client_secret redirect_uri)a
|
||||
|
||||
def secret_for([:authentication, :tokens, :signing_secret], AshHq.Accounts.User, _) do
|
||||
Application.fetch_env(:ash_hq, :token_signing_secret)
|
||||
end
|
||||
|
||||
def secret_for([:authentication, :strategies, :github, key], AshHq.Accounts.User, _)
|
||||
when key in @github_secret_keys do
|
||||
with {:ok, value} <- Application.fetch_env(:ash_hq, :github) do
|
||||
Keyword.fetch(value, key)
|
||||
end
|
||||
end
|
||||
end
|
Binary file not shown.
Before Width: | Height: | Size: 850 KiB |
|
@ -5,35 +5,6 @@ defmodule AshHq.Docs.Dsl do
|
|||
data_layer: AshPostgres.DataLayer,
|
||||
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
|
||||
|
||||
postgres do
|
||||
table "dsls"
|
||||
repo AshHq.Repo
|
||||
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
search do
|
||||
doc_attribute :doc
|
||||
|
||||
load_for_search [
|
||||
:extension_order,
|
||||
:extension_type,
|
||||
:extension_name,
|
||||
:version_name,
|
||||
:library_name,
|
||||
:library_id
|
||||
]
|
||||
|
||||
sanitized_name_attribute :sanitized_path
|
||||
use_path_for_name? true
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes doc: :doc_html
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
|
@ -74,6 +45,26 @@ defmodule AshHq.Docs.Dsl do
|
|||
timestamps()
|
||||
end
|
||||
|
||||
search do
|
||||
doc_attribute :doc
|
||||
|
||||
load_for_search [
|
||||
:extension_order,
|
||||
:extension_type,
|
||||
:extension_name,
|
||||
:version_name,
|
||||
:library_name,
|
||||
:library_id
|
||||
]
|
||||
|
||||
sanitized_name_attribute :sanitized_path
|
||||
use_path_for_name? true
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes doc: :doc_html
|
||||
end
|
||||
|
||||
relationships do
|
||||
belongs_to :library_version, AshHq.Docs.LibraryVersion do
|
||||
allow_nil? true
|
||||
|
@ -88,6 +79,15 @@ defmodule AshHq.Docs.Dsl do
|
|||
has_many :dsls, __MODULE__
|
||||
end
|
||||
|
||||
postgres do
|
||||
table "dsls"
|
||||
repo AshHq.Repo
|
||||
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:destroy]
|
||||
|
||||
|
|
|
@ -5,24 +5,6 @@ defmodule AshHq.Docs.Extension do
|
|||
data_layer: AshPostgres.DataLayer,
|
||||
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
|
||||
|
||||
postgres do
|
||||
table "extensions"
|
||||
repo AshHq.Repo
|
||||
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
search do
|
||||
doc_attribute :doc
|
||||
load_for_search library_version: [:library_display_name, :library_name]
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes doc: :doc_html
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
|
@ -58,6 +40,15 @@ defmodule AshHq.Docs.Extension do
|
|||
timestamps()
|
||||
end
|
||||
|
||||
search do
|
||||
doc_attribute :doc
|
||||
load_for_search library_version: [:library_display_name, :library_name]
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes doc: :doc_html
|
||||
end
|
||||
|
||||
relationships do
|
||||
belongs_to :library_version, AshHq.Docs.LibraryVersion do
|
||||
allow_nil? true
|
||||
|
@ -67,6 +58,15 @@ defmodule AshHq.Docs.Extension do
|
|||
has_many :options, AshHq.Docs.Option
|
||||
end
|
||||
|
||||
postgres do
|
||||
table "extensions"
|
||||
repo AshHq.Repo
|
||||
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:update, :destroy]
|
||||
|
||||
|
|
|
@ -5,35 +5,6 @@ defmodule AshHq.Docs.Function do
|
|||
data_layer: AshPostgres.DataLayer,
|
||||
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
|
||||
|
||||
postgres do
|
||||
table "functions"
|
||||
repo AshHq.Repo
|
||||
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
search do
|
||||
doc_attribute :doc
|
||||
|
||||
load_for_search [
|
||||
:version_name,
|
||||
:library_name,
|
||||
:module_name,
|
||||
:library_id
|
||||
]
|
||||
|
||||
type "Code"
|
||||
|
||||
show_docs_on :module_sanitized_name
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes doc: :doc_html, heads: :heads_html
|
||||
header_ids? false
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
|
@ -79,6 +50,26 @@ defmodule AshHq.Docs.Function do
|
|||
timestamps()
|
||||
end
|
||||
|
||||
search do
|
||||
doc_attribute :doc
|
||||
|
||||
load_for_search [
|
||||
:version_name,
|
||||
:library_name,
|
||||
:module_name,
|
||||
:library_id
|
||||
]
|
||||
|
||||
type "Code"
|
||||
|
||||
show_docs_on :module_sanitized_name
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes doc: :doc_html, heads: :heads_html
|
||||
header_ids? false
|
||||
end
|
||||
|
||||
relationships do
|
||||
belongs_to :library_version, AshHq.Docs.LibraryVersion do
|
||||
allow_nil? true
|
||||
|
@ -89,6 +80,15 @@ defmodule AshHq.Docs.Function do
|
|||
end
|
||||
end
|
||||
|
||||
postgres do
|
||||
table "functions"
|
||||
repo AshHq.Repo
|
||||
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:update, :destroy]
|
||||
|
||||
|
|
|
@ -9,42 +9,6 @@ defmodule AshHq.Docs.Guide do
|
|||
AshAdmin.Resource
|
||||
]
|
||||
|
||||
postgres do
|
||||
repo AshHq.Repo
|
||||
table "guides"
|
||||
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
search do
|
||||
doc_attribute :text
|
||||
show_docs_on [:sanitized_name, :sanitized_route]
|
||||
type "Guides"
|
||||
load_for_search library_version: [:library_name, :library_display_name]
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes text: :text_html
|
||||
end
|
||||
|
||||
graphql do
|
||||
type :guide
|
||||
|
||||
queries do
|
||||
list :list_guides, :read_for_version
|
||||
end
|
||||
end
|
||||
|
||||
admin do
|
||||
form do
|
||||
field :text do
|
||||
type :markdown
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
|
@ -89,12 +53,48 @@ defmodule AshHq.Docs.Guide do
|
|||
timestamps()
|
||||
end
|
||||
|
||||
search do
|
||||
doc_attribute :text
|
||||
show_docs_on [:sanitized_name, :sanitized_route]
|
||||
type "Guides"
|
||||
load_for_search library_version: [:library_name, :library_display_name]
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes text: :text_html
|
||||
end
|
||||
|
||||
graphql do
|
||||
type :guide
|
||||
|
||||
queries do
|
||||
list :list_guides, :read_for_version
|
||||
end
|
||||
end
|
||||
|
||||
admin do
|
||||
form do
|
||||
field :text do
|
||||
type :markdown
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
relationships do
|
||||
belongs_to :library_version, AshHq.Docs.LibraryVersion do
|
||||
allow_nil? true
|
||||
end
|
||||
end
|
||||
|
||||
postgres do
|
||||
repo AshHq.Repo
|
||||
table "guides"
|
||||
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:create, :update, :destroy]
|
||||
|
||||
|
|
|
@ -3,11 +3,6 @@ defmodule AshHq.Docs.Library do
|
|||
use AshHq.Resource,
|
||||
data_layer: AshPostgres.DataLayer
|
||||
|
||||
postgres do
|
||||
table "libraries"
|
||||
repo AshHq.Repo
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
|
@ -37,6 +32,11 @@ defmodule AshHq.Docs.Library do
|
|||
has_many :versions, AshHq.Docs.LibraryVersion
|
||||
end
|
||||
|
||||
postgres do
|
||||
table "libraries"
|
||||
repo AshHq.Repo
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:read, :create, :update, :destroy]
|
||||
|
||||
|
|
|
@ -5,17 +5,6 @@ defmodule AshHq.Docs.LibraryVersion do
|
|||
data_layer: AshPostgres.DataLayer,
|
||||
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
|
||||
|
||||
postgres do
|
||||
table "library_versions"
|
||||
repo AshHq.Repo
|
||||
end
|
||||
|
||||
search do
|
||||
name_attribute :version
|
||||
library_version_attribute :id
|
||||
load_for_search [:library_name, :library_display_name]
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
|
@ -26,6 +15,12 @@ defmodule AshHq.Docs.LibraryVersion do
|
|||
timestamps()
|
||||
end
|
||||
|
||||
search do
|
||||
name_attribute :version
|
||||
library_version_attribute :id
|
||||
load_for_search [:library_name, :library_display_name]
|
||||
end
|
||||
|
||||
relationships do
|
||||
belongs_to :library, AshHq.Docs.Library do
|
||||
allow_nil? true
|
||||
|
@ -37,6 +32,11 @@ defmodule AshHq.Docs.LibraryVersion do
|
|||
has_many :mix_tasks, AshHq.Docs.MixTask
|
||||
end
|
||||
|
||||
postgres do
|
||||
table "library_versions"
|
||||
repo AshHq.Repo
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:update, :destroy]
|
||||
|
||||
|
|
|
@ -5,33 +5,6 @@ defmodule AshHq.Docs.MixTask do
|
|||
data_layer: AshPostgres.DataLayer,
|
||||
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
|
||||
|
||||
postgres do
|
||||
table "mix_tasks"
|
||||
repo AshHq.Repo
|
||||
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
search do
|
||||
doc_attribute :doc
|
||||
|
||||
load_for_search [
|
||||
:version_name,
|
||||
:library_name,
|
||||
:library_id
|
||||
]
|
||||
|
||||
item_type "Mix Task"
|
||||
|
||||
type "Mix Tasks"
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes doc: :doc_html
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
|
@ -64,12 +37,39 @@ defmodule AshHq.Docs.MixTask do
|
|||
timestamps()
|
||||
end
|
||||
|
||||
search do
|
||||
doc_attribute :doc
|
||||
|
||||
load_for_search [
|
||||
:version_name,
|
||||
:library_name,
|
||||
:library_id
|
||||
]
|
||||
|
||||
item_type "Mix Task"
|
||||
|
||||
type "Mix Tasks"
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes doc: :doc_html
|
||||
end
|
||||
|
||||
relationships do
|
||||
belongs_to :library_version, AshHq.Docs.LibraryVersion do
|
||||
allow_nil? true
|
||||
end
|
||||
end
|
||||
|
||||
postgres do
|
||||
table "mix_tasks"
|
||||
repo AshHq.Repo
|
||||
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:update, :destroy]
|
||||
|
||||
|
|
|
@ -5,31 +5,6 @@ defmodule AshHq.Docs.Module do
|
|||
data_layer: AshPostgres.DataLayer,
|
||||
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
|
||||
|
||||
postgres do
|
||||
table "modules"
|
||||
repo AshHq.Repo
|
||||
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
search do
|
||||
doc_attribute :doc
|
||||
|
||||
load_for_search [
|
||||
:version_name,
|
||||
:library_name,
|
||||
:library_id
|
||||
]
|
||||
|
||||
type "Code"
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes doc: :doc_html
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
|
@ -62,6 +37,22 @@ defmodule AshHq.Docs.Module do
|
|||
timestamps()
|
||||
end
|
||||
|
||||
search do
|
||||
doc_attribute :doc
|
||||
|
||||
load_for_search [
|
||||
:version_name,
|
||||
:library_name,
|
||||
:library_id
|
||||
]
|
||||
|
||||
type "Code"
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes doc: :doc_html
|
||||
end
|
||||
|
||||
relationships do
|
||||
belongs_to :library_version, AshHq.Docs.LibraryVersion do
|
||||
allow_nil? true
|
||||
|
@ -70,6 +61,15 @@ defmodule AshHq.Docs.Module do
|
|||
has_many :functions, AshHq.Docs.Function
|
||||
end
|
||||
|
||||
postgres do
|
||||
table "modules"
|
||||
repo AshHq.Repo
|
||||
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:update, :destroy]
|
||||
|
||||
|
|
|
@ -5,37 +5,6 @@ defmodule AshHq.Docs.Option do
|
|||
data_layer: AshPostgres.DataLayer,
|
||||
extensions: [AshHq.Docs.Extensions.Search, AshHq.Docs.Extensions.RenderMarkdown]
|
||||
|
||||
postgres do
|
||||
table "options"
|
||||
repo AshHq.Repo
|
||||
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
search do
|
||||
doc_attribute :doc
|
||||
|
||||
load_for_search [
|
||||
:extension_order,
|
||||
:extension_type,
|
||||
:extension_name,
|
||||
:version_name,
|
||||
:library_name,
|
||||
:library_id
|
||||
]
|
||||
|
||||
sanitized_name_attribute :sanitized_path
|
||||
use_path_for_name? true
|
||||
add_name_to_path? false
|
||||
show_docs_on :dsl_sanitized_path
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes doc: :doc_html
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
|
@ -76,6 +45,28 @@ defmodule AshHq.Docs.Option do
|
|||
timestamps()
|
||||
end
|
||||
|
||||
search do
|
||||
doc_attribute :doc
|
||||
|
||||
load_for_search [
|
||||
:extension_order,
|
||||
:extension_type,
|
||||
:extension_name,
|
||||
:version_name,
|
||||
:library_name,
|
||||
:library_id
|
||||
]
|
||||
|
||||
sanitized_name_attribute :sanitized_path
|
||||
use_path_for_name? true
|
||||
add_name_to_path? false
|
||||
show_docs_on :dsl_sanitized_path
|
||||
end
|
||||
|
||||
render_markdown do
|
||||
render_attributes doc: :doc_html
|
||||
end
|
||||
|
||||
relationships do
|
||||
belongs_to :dsl, AshHq.Docs.Dsl do
|
||||
allow_nil? true
|
||||
|
@ -90,6 +81,15 @@ defmodule AshHq.Docs.Option do
|
|||
end
|
||||
end
|
||||
|
||||
postgres do
|
||||
table "options"
|
||||
repo AshHq.Repo
|
||||
|
||||
references do
|
||||
reference :library_version, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:update, :destroy]
|
||||
|
||||
|
|
|
@ -17,6 +17,12 @@ defmodule AshHqWeb do
|
|||
and import those modules here.
|
||||
"""
|
||||
|
||||
def verified_routes do
|
||||
quote do
|
||||
unquote(use_verified_routes())
|
||||
end
|
||||
end
|
||||
|
||||
def controller do
|
||||
quote do
|
||||
use Phoenix.Controller, namespace: AshHqWeb
|
||||
|
@ -92,6 +98,7 @@ defmodule AshHqWeb do
|
|||
|
||||
# Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc)
|
||||
import Phoenix.LiveView.Helpers
|
||||
import Phoenix.Component
|
||||
|
||||
# Import basic rendering functionality (render, render_layout, etc)
|
||||
import Phoenix.View
|
||||
|
@ -99,9 +106,24 @@ defmodule AshHqWeb do
|
|||
import AshHqWeb.ErrorHelpers
|
||||
import AshHqWeb.Gettext
|
||||
alias AshHqWeb.Router.Helpers, as: Routes
|
||||
unquote(use_verified_routes())
|
||||
end
|
||||
end
|
||||
|
||||
@spec use_verified_routes :: Macro.t()
|
||||
def use_verified_routes do
|
||||
quote do
|
||||
use Phoenix.VerifiedRoutes,
|
||||
endpoint: AshHqWeb.Endpoint,
|
||||
router: AshHqWeb.Router,
|
||||
statics: AshHqWeb.static_paths()
|
||||
end
|
||||
end
|
||||
|
||||
def static_paths do
|
||||
~w(assets fonts images favicon.ico robots.txt)
|
||||
end
|
||||
|
||||
@doc """
|
||||
When used, dispatch to the appropriate controller/view/etc.
|
||||
"""
|
||||
|
|
9
lib/ash_hq_web/auth_overrides.ex
Normal file
9
lib/ash_hq_web/auth_overrides.ex
Normal file
|
@ -0,0 +1,9 @@
|
|||
defmodule AshHqWeb.AuthOverrides do
|
||||
@moduledoc "UI overrides for authentication views"
|
||||
use AshAuthentication.Phoenix.Overrides
|
||||
alias AshAuthentication.Phoenix.Components
|
||||
|
||||
override Components.HorizontalRule do
|
||||
set :root_class, "hidden"
|
||||
end
|
||||
end
|
|
@ -1,11 +1,11 @@
|
|||
defmodule AshHqWeb.Components.AppView.TopBar do
|
||||
@moduledoc "The global top navigation bar"
|
||||
use Surface.Component
|
||||
use AshHqWeb, :component
|
||||
|
||||
prop live_action, :atom, required: true
|
||||
prop toggle_theme, :event, required: true
|
||||
prop configured_theme, :string, required: true
|
||||
prop current_user, :any
|
||||
prop(live_action, :atom, required: true)
|
||||
prop(toggle_theme, :event, required: true)
|
||||
prop(configured_theme, :string, required: true)
|
||||
prop(current_user, :any)
|
||||
|
||||
alias AshHqWeb.Components.SearchBar
|
||||
alias AshHqWeb.Router.Helpers, as: Routes
|
||||
|
@ -150,7 +150,7 @@ defmodule AshHqWeb.Components.AppView.TopBar do
|
|||
</LivePatch>
|
||||
</div>
|
||||
<div class="py-1" role="none">
|
||||
<Form for={:sign_out} action={Routes.user_session_path(AshHqWeb.Endpoint, :delete)} method="post">
|
||||
<Form for={:sign_out} action={~p'/sign-out'} method="get">
|
||||
<button
|
||||
label="logout"
|
||||
type="submit"
|
||||
|
@ -165,15 +165,12 @@ defmodule AshHqWeb.Components.AppView.TopBar do
|
|||
</div>
|
||||
{#else}
|
||||
<div class="py-1" role="none">
|
||||
<LivePatch
|
||||
to={Routes.app_view_path(AshHqWeb.Endpoint, :log_in)}
|
||||
class="dark:text-white group flex items-center px-4 py-2 text-sm"
|
||||
>
|
||||
<a href={~p'/sign-in'} class="dark:text-white group flex items-center px-4 py-2 text-sm">
|
||||
<div class="flex items-center">
|
||||
<Heroicons.Outline.UserAddIcon class="mr-3 h-5 w-5 text-base-light-400 group-hover:text-base-light-500" />
|
||||
Sign In
|
||||
</div>
|
||||
</LivePatch>
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
74
lib/ash_hq_web/controllers/auth_controller.ex
Normal file
74
lib/ash_hq_web/controllers/auth_controller.ex
Normal file
|
@ -0,0 +1,74 @@
|
|||
defmodule AshHqWeb.AuthController do
|
||||
use AshHqWeb, :controller
|
||||
use AshAuthentication.Phoenix.Controller
|
||||
|
||||
require Logger
|
||||
|
||||
def success(conn, _activity, user, _token) do
|
||||
return_to = get_session(conn, :return_to) || "/"
|
||||
|
||||
conn
|
||||
|> delete_session(:return_to)
|
||||
|> store_in_session(user)
|
||||
|> assign(:current_user, user)
|
||||
|> redirect(to: return_to)
|
||||
end
|
||||
|
||||
def failure(
|
||||
conn,
|
||||
{:password, :sign_in},
|
||||
%AshAuthentication.Errors.AuthenticationFailed{}
|
||||
) do
|
||||
conn
|
||||
|> put_flash(
|
||||
:error,
|
||||
"Username or password is incorrect"
|
||||
)
|
||||
|> redirect(to: "/sign-in")
|
||||
end
|
||||
|
||||
def failure(conn, activity, reason) do
|
||||
stacktrace =
|
||||
case reason do
|
||||
%{stacktrace: %{stacktrace: stacktrace}} -> stacktrace
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
Logger.error("""
|
||||
Something went wrong in authentication
|
||||
|
||||
activity: #{inspect(activity)}
|
||||
|
||||
reason: #{Exception.format(:error, reason, stacktrace || [])}
|
||||
""")
|
||||
|
||||
conn
|
||||
|> put_flash(
|
||||
:error,
|
||||
"Something went wrong"
|
||||
)
|
||||
|> redirect(to: "/sign-in")
|
||||
end
|
||||
|
||||
def sign_out(conn, _params) do
|
||||
return_to = get_session(conn, :return_to) || "/"
|
||||
|
||||
token = Plug.Conn.get_session(conn, "user_token")
|
||||
|
||||
if token do
|
||||
AshHq.Accounts.UserToken
|
||||
|> AshAuthentication.TokenResource.Actions.get_token(%{"token" => token})
|
||||
|> case do
|
||||
{:ok, [token]} ->
|
||||
AshHq.Accounts.UserToken.destroy!(token, authorize?: false)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
conn
|
||||
|> clear_session()
|
||||
|> redirect(to: return_to)
|
||||
end
|
||||
end
|
|
@ -1,78 +0,0 @@
|
|||
defmodule AshHqWeb.UserConfirmationController do
|
||||
use AshHqWeb, :controller
|
||||
|
||||
alias AshHq.Accounts
|
||||
require Ash.Query
|
||||
|
||||
def create(conn, %{"user" => %{"email" => email}}) do
|
||||
user =
|
||||
AshHq.Accounts.User
|
||||
|> Ash.Query.filter(email == ^email)
|
||||
|> AshHq.Accounts.read_one!(authorize?: false)
|
||||
|
||||
if user do
|
||||
user
|
||||
|> Ash.Changeset.for_update(
|
||||
:deliver_user_confirmation_instructions,
|
||||
%{
|
||||
confirmation_url_fun: &Routes.user_confirmation_url(conn, :confirm, &1)
|
||||
},
|
||||
authorize?: false
|
||||
)
|
||||
|> Accounts.update()
|
||||
end
|
||||
|
||||
# Regardless of the outcome, show an impartial success/error message.
|
||||
conn
|
||||
|> put_flash(
|
||||
:info,
|
||||
"If your email is in our system and it has not been confirmed yet, " <>
|
||||
"you will receive an email with instructions shortly."
|
||||
)
|
||||
|> redirect(to: Routes.app_view_path(AshHqWeb.Endpoint, :home))
|
||||
end
|
||||
|
||||
# Do not log in the user after confirmation to avoid a
|
||||
# leaked token giving the user access to the account.
|
||||
def confirm(conn, %{"token" => token}) do
|
||||
result =
|
||||
AshHq.Accounts.User
|
||||
|> Ash.Query.for_read(:with_verified_email_token, %{token: token, context: "confirm"},
|
||||
authorize?: false
|
||||
)
|
||||
|> AshHq.Accounts.read_one!()
|
||||
|> case do
|
||||
nil ->
|
||||
:error
|
||||
|
||||
user ->
|
||||
user
|
||||
|> Ash.Changeset.for_update(:confirm, %{delete_confirm_tokens: true, token: token},
|
||||
authorize?: false
|
||||
)
|
||||
|> AshHq.Accounts.update()
|
||||
end
|
||||
|
||||
case result do
|
||||
{:ok, _} ->
|
||||
conn
|
||||
|> put_flash(:info, "Account confirmed successfully.")
|
||||
|> redirect(to: Routes.app_view_path(AshHqWeb.Endpoint, :home))
|
||||
|
||||
:error ->
|
||||
# If there is a current user and the account was already confirmed,
|
||||
# then odds are that the confirmation link was already visited, either
|
||||
# by some automation or by the user themselves, so we redirect without
|
||||
# a warning message.
|
||||
case conn.assigns do
|
||||
%{current_user: %{confirmed_at: confirmed_at}} when not is_nil(confirmed_at) ->
|
||||
redirect(conn, to: Routes.app_view_path(AshHqWeb.Endpoint, :home))
|
||||
|
||||
%{} ->
|
||||
conn
|
||||
|> put_flash(:error, "Account confirmation link is invalid or it has expired.")
|
||||
|> redirect(to: Routes.app_view_path(AshHqWeb.Endpoint, :home))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,41 +0,0 @@
|
|||
defmodule AshHqWeb.UserSessionController do
|
||||
use AshHqWeb, :controller
|
||||
|
||||
alias AshHqWeb.UserAuth
|
||||
|
||||
def log_in(conn, %{"log_in" => %{"token" => token} = params}) do
|
||||
token = Base.url_decode64!(token, padding: false)
|
||||
UserAuth.log_in_with_token(conn, token, params["remember_me"] == "true", params["return_to"])
|
||||
end
|
||||
|
||||
def log_in(conn, %{"log_in" => %{"email" => email, "password" => password} = params}) do
|
||||
AshHq.Accounts.User
|
||||
|> Ash.Query.for_read(:by_email_and_password, %{email: email, password: password},
|
||||
authorize?: false
|
||||
)
|
||||
|> AshHq.Accounts.read_one()
|
||||
|> case do
|
||||
{:ok, nil} ->
|
||||
redirect(conn, to: "/")
|
||||
|
||||
{:ok, user} ->
|
||||
token = AshHqWeb.UserAuth.create_token_for_user(user)
|
||||
|
||||
UserAuth.log_in_with_token(
|
||||
conn,
|
||||
token,
|
||||
params["remember_me"] == "true",
|
||||
params["return_to"]
|
||||
)
|
||||
|
||||
{:error, _} ->
|
||||
redirect(conn, to: "/")
|
||||
end
|
||||
end
|
||||
|
||||
def delete(conn, _params) do
|
||||
conn
|
||||
|> put_flash(:info, "Logged out successfully.")
|
||||
|> UserAuth.log_out_user()
|
||||
end
|
||||
end
|
|
@ -1,20 +0,0 @@
|
|||
defmodule AshHqWeb.UserSettingsController do
|
||||
use AshHqWeb, :controller
|
||||
|
||||
def confirm_email(conn, %{"token" => token}) do
|
||||
conn.assigns.current_user
|
||||
|> Ash.Changeset.for_update(:change_email, %{token: token}, authorize?: false)
|
||||
|> AshHq.Accounts.update()
|
||||
|> case do
|
||||
{:ok, _} ->
|
||||
conn
|
||||
|> put_flash(:info, "Email changed successfully.")
|
||||
|> redirect(to: Routes.app_view_path(conn, :user_settings))
|
||||
|
||||
{:error, _form} ->
|
||||
conn
|
||||
|> put_flash(:error, "Email change link is invalid or it has expired.")
|
||||
|> redirect(to: Routes.app_view_path(conn, :user_settings))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -20,7 +20,7 @@ defmodule AshHqWeb.Endpoint do
|
|||
at: "/",
|
||||
from: :ash_hq,
|
||||
gzip: false,
|
||||
only: ~w(assets fonts images favicon.ico robots.txt)
|
||||
only: AshHqWeb.static_paths()
|
||||
|
||||
# Need to figure out CSP yet
|
||||
# plug PlugContentSecurityPolicy
|
||||
|
|
|
@ -3,48 +3,22 @@ defmodule AshHqWeb.LiveUserAuth do
|
|||
Helpers for authenticating users in liveviews
|
||||
"""
|
||||
|
||||
alias AshHqWeb.Router.Helpers, as: Routes
|
||||
import Phoenix.Component
|
||||
use AshHqWeb, :verified_routes
|
||||
|
||||
@doc """
|
||||
Sets the current user on each mount of a liveview
|
||||
"""
|
||||
def on_mount(:live_user, _params, session, socket) do
|
||||
{:cont,
|
||||
Phoenix.Component.assign(
|
||||
socket,
|
||||
:current_user,
|
||||
AshHqWeb.UserAuth.user_for_session_token(session["user_token"])
|
||||
)}
|
||||
end
|
||||
|
||||
def on_mount(:live_user_required, _params, session, socket) do
|
||||
case AshHqWeb.UserAuth.user_for_session_token(session["user_token"]) do
|
||||
nil ->
|
||||
{:halt, Phoenix.LiveView.redirect(socket, to: "/users/log_in")}
|
||||
|
||||
user ->
|
||||
{:cont,
|
||||
Phoenix.Component.assign(
|
||||
socket,
|
||||
:current_user,
|
||||
user
|
||||
)}
|
||||
def on_mount(:live_user_optional, _params, _session, socket) do
|
||||
if socket.assigns[:current_user] do
|
||||
{:cont, socket}
|
||||
else
|
||||
{:cont, assign(socket, :current_user, nil)}
|
||||
end
|
||||
end
|
||||
|
||||
def on_mount(:live_user_not_allowed, _params, session, socket) do
|
||||
case AshHqWeb.UserAuth.user_for_session_token(session["user_token"]) do
|
||||
nil ->
|
||||
{:cont,
|
||||
Phoenix.Component.assign(
|
||||
socket,
|
||||
:current_user,
|
||||
nil
|
||||
)}
|
||||
|
||||
_user ->
|
||||
{:halt,
|
||||
Phoenix.LiveView.redirect(socket, to: Routes.app_view_path(AshHqWeb.Endpoint, :home))}
|
||||
def on_mount(:live_user_required, _params, _session, socket) do
|
||||
if socket.assigns[:current_user] do
|
||||
{:cont, socket}
|
||||
else
|
||||
{:halt, Phoenix.LiveView.redirect(socket, to: ~p"/sign-in")}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,159 +0,0 @@
|
|||
defmodule AshHqWeb.Pages.LogIn do
|
||||
@moduledoc "Log in page"
|
||||
use Surface.LiveComponent
|
||||
|
||||
alias Surface.Components.{Form, LivePatch}
|
||||
|
||||
alias Surface.Components.Form.{
|
||||
Checkbox,
|
||||
ErrorTag,
|
||||
Field,
|
||||
HiddenInput,
|
||||
Label,
|
||||
PasswordInput,
|
||||
Submit,
|
||||
TextInput
|
||||
}
|
||||
|
||||
alias AshHqWeb.Router.Helpers, as: Routes
|
||||
|
||||
data log_in_form, :map
|
||||
data token, :string, default: nil
|
||||
data trigger_action, :boolean, default: false
|
||||
|
||||
def render(assigns) do
|
||||
~F"""
|
||||
<div class="container flex flex-wrap mx-auto">
|
||||
<div class="w-full md:w-2/3 ml-2">
|
||||
<Form
|
||||
class="space-y-8"
|
||||
opts={id: @log_in_form.id}
|
||||
for={@log_in_form}
|
||||
change="validate_log_in"
|
||||
submit="log_in"
|
||||
trigger_action={@trigger_action}
|
||||
action={Routes.user_session_path(AshHqWeb.Endpoint, :log_in)}
|
||||
>
|
||||
{#if @token}
|
||||
<Field name={:token}>
|
||||
<HiddenInput value={@token} />
|
||||
</Field>
|
||||
{/if}
|
||||
<div class="space-y-8 sm:space-y-5">
|
||||
<div>
|
||||
<h3 class="text-lg leading-6 font-medium">Log In</h3>
|
||||
{#if @log_in_form.submitted_once?}
|
||||
<div class="alert alert-danger">
|
||||
<p>Oops, something went wrong! Please check the errors below.</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-6 sm:mt-5 space-y-6 sm:space-y-5">
|
||||
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5">
|
||||
<Field name={:email}>
|
||||
<Label class="block text-sm font-medium sm:mt-px sm:pt-2">Email</Label>
|
||||
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<div class="max-w-lg flex rounded-md shadow-sm">
|
||||
<TextInput
|
||||
class="flex-1 text-black block w-full focus:ring-primary-light-600 focus:border-primary-light-600 min-w-0 rounded-md sm:text-sm border-base-light-300"
|
||||
opts={autocomplete: "email"}
|
||||
/>
|
||||
</div>
|
||||
{#if @log_in_form.submitted_once?}
|
||||
<ErrorTag />
|
||||
{/if}
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5">
|
||||
<Field name={:password}>
|
||||
<Label class="block text-sm font-medium sm:mt-px sm:pt-2">Password</Label>
|
||||
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<div class="max-w-lg flex rounded-md shadow-sm">
|
||||
<PasswordInput
|
||||
value={AshPhoenix.Form.value(@log_in_form, :password)}
|
||||
class="flex-1 text-black block w-full focus:ring-primary-light-500 focus:border-primary-light-600 min-w-0 rounded-md sm:text-sm border-base-light-300"
|
||||
/>
|
||||
</div>
|
||||
{#if @log_in_form.submitted_once?}
|
||||
<ErrorTag />
|
||||
{/if}
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
<Field name={:remember_me}>
|
||||
<Label class="block text-sm font-medium sm:mt-px sm:pt-2">Remember Me</Label>
|
||||
<div class="mt-1">
|
||||
<div class="flex rounded-md shadow-sm">
|
||||
<Checkbox class="text-black block focus:ring--500 focus:border-primary-light-600 min-w-0 rounded-md sm:text-sm border-base-light-300" />
|
||||
</div>
|
||||
{#if @log_in_form.submitted_once?}
|
||||
<ErrorTag />
|
||||
{/if}
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-5">
|
||||
<div class="flex justify-end">
|
||||
<Submit class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md bg-primary-light-600 hover:bg-primary-light-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-light-600 text-white">Log In</Submit>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
<LivePatch to={Routes.app_view_path(AshHqWeb.Endpoint, :register)}>Register?</LivePatch> |
|
||||
<LivePatch to={Routes.app_view_path(AshHqWeb.Endpoint, :reset_password)}>Forgot Password?</LivePatch>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
{:ok, socket |> assign(assigns) |> assign_form()}
|
||||
end
|
||||
|
||||
defp assign_form(socket) do
|
||||
assign(socket,
|
||||
log_in_form:
|
||||
AshPhoenix.Form.for_read(
|
||||
AshHq.Accounts.User,
|
||||
:by_email_and_password,
|
||||
as: "log_in",
|
||||
api: AshHq.Accounts
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("validate_log_in", %{"log_in" => params}, socket) do
|
||||
{:noreply,
|
||||
assign(socket,
|
||||
log_in_form: AshPhoenix.Form.validate(socket.assigns.log_in_form, params)
|
||||
)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("log_in", %{"log_in" => params}, socket) do
|
||||
case AshPhoenix.Form.submit(socket.assigns.log_in_form, params: params, read_one?: true) do
|
||||
{:ok, nil} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:error, "Invalid username or password")
|
||||
|> push_redirect(to: Routes.app_view_path(AshHqWeb.Endpoint, :log_in))}
|
||||
|
||||
{:ok, user} ->
|
||||
token = AshHqWeb.UserAuth.create_token_for_user(user)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:trigger_action, true)
|
||||
|> assign(:token, Base.url_encode64(token, padding: false))}
|
||||
|
||||
{:error, form} ->
|
||||
{:noreply, assign(socket, log_in_form: form)}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,149 +0,0 @@
|
|||
defmodule AshHqWeb.Pages.Register do
|
||||
@moduledoc "Log in page"
|
||||
use Surface.LiveComponent
|
||||
|
||||
alias Surface.Components.{Form, LivePatch}
|
||||
|
||||
alias Surface.Components.Form.{
|
||||
Checkbox,
|
||||
ErrorTag,
|
||||
Field,
|
||||
Label,
|
||||
PasswordInput,
|
||||
Submit,
|
||||
TextInput
|
||||
}
|
||||
|
||||
alias AshHqWeb.Router.Helpers, as: Routes
|
||||
|
||||
data register_form, :map
|
||||
|
||||
def render(assigns) do
|
||||
~F"""
|
||||
<div class="container flex flex-wrap mx-auto">
|
||||
<div class="w-full md:w-2/3 ml-2">
|
||||
<Form class="space-y-8" for={@register_form} change="validate_register" submit="register">
|
||||
<div class="space-y-8 sm:space-y-5">
|
||||
<div>
|
||||
<h3 class="text-lg leading-6 font-medium">Register</h3>
|
||||
{#if @register_form.submitted_once?}
|
||||
<div class="alert alert-danger">
|
||||
<p>Oops, something went wrong! Please check the errors below.</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-6 sm:mt-5 space-y-6 sm:space-y-5">
|
||||
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5">
|
||||
<Field name={:email}>
|
||||
<Label class="block text-sm font-medium sm:mt-px sm:pt-2">Email</Label>
|
||||
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<div class="max-w-lg flex rounded-md shadow-sm">
|
||||
<TextInput
|
||||
class="flex-1 text-black block w-full focus:ring-primary-light-600 focus:border-primary-light-600 min-w-0 rounded-md sm:text-sm border-base-light-300"
|
||||
opts={autocomplete: "email"}
|
||||
/>
|
||||
</div>
|
||||
{#if @register_form.submitted_once?}
|
||||
<ErrorTag />
|
||||
{/if}
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5">
|
||||
<Field name={:password}>
|
||||
<Label class="block text-sm font-medium sm:mt-px sm:pt-2">Password</Label>
|
||||
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<div class="max-w-lg flex rounded-md shadow-sm">
|
||||
<PasswordInput
|
||||
value={AshPhoenix.Form.value(@register_form, :password)}
|
||||
class="flex-1 text-black block w-full focus:ring--500 focus:border-primary-light-600 min-w-0 rounded-md sm:text-sm border-base-light-300"
|
||||
/>
|
||||
</div>
|
||||
{#if @register_form.submitted_once?}
|
||||
<ErrorTag />
|
||||
{/if}
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
<Field name={:remember_me}>
|
||||
<Label class="block text-sm font-medium sm:mt-px sm:pt-2">Remember Me</Label>
|
||||
<div class="mt-1">
|
||||
<div class="flex rounded-md shadow-sm">
|
||||
<Checkbox class="text-black block focus:ring-primary-light-500 focus:border-primary-light-600 min-w-0 rounded-md sm:text-sm border-base-light-300" />
|
||||
</div>
|
||||
{#if @register_form.submitted_once?}
|
||||
<ErrorTag />
|
||||
{/if}
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-5">
|
||||
<div class="flex justify-end">
|
||||
<Submit class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md bg-primary-light-600 hover:bg-primary-light-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-light-600 text-white">Sign Up</Submit>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
<LivePatch to={Routes.app_view_path(AshHqWeb.Endpoint, :log_in)}>Already Have an account?</LivePatch>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
{:ok, socket |> assign(assigns) |> assign_form()}
|
||||
end
|
||||
|
||||
defp assign_form(socket) do
|
||||
assign(socket,
|
||||
register_form:
|
||||
AshPhoenix.Form.for_create(
|
||||
AshHq.Accounts.User,
|
||||
:register,
|
||||
as: "register",
|
||||
api: AshHq.Accounts
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("validate_register", %{"register" => params}, socket) do
|
||||
{:noreply,
|
||||
assign(socket,
|
||||
register_form: AshPhoenix.Form.validate(socket.assigns.register_form, params)
|
||||
)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("register", %{"register" => params}, socket) do
|
||||
params =
|
||||
Map.put(
|
||||
params,
|
||||
"confirmation_url_fun",
|
||||
&Routes.user_confirmation_url(AshHqWeb.Endpoint, :confirm, &1)
|
||||
)
|
||||
|
||||
case AshPhoenix.Form.submit(socket.assigns.register_form, params: params) do
|
||||
{:ok, user} ->
|
||||
token = AshHqWeb.UserAuth.create_token_for_user(user)
|
||||
|
||||
{:noreply,
|
||||
redirect(socket,
|
||||
to:
|
||||
Routes.user_session_path(AshHqWeb.Endpoint, :log_in, %{
|
||||
"log_in" => %{
|
||||
"token" => Base.url_encode64(token, padding: false),
|
||||
"remember_me" => params["remember_me"] || "false"
|
||||
}
|
||||
})
|
||||
)}
|
||||
|
||||
{:error, form} ->
|
||||
{:noreply, assign(socket, register_form: form)}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,195 +0,0 @@
|
|||
defmodule AshHqWeb.Pages.ResetPassword do
|
||||
@moduledoc "Log in page"
|
||||
use Surface.LiveComponent
|
||||
|
||||
alias Surface.Components.{Form, LivePatch}
|
||||
|
||||
alias Surface.Components.Form.{
|
||||
ErrorTag,
|
||||
Field,
|
||||
Label,
|
||||
PasswordInput,
|
||||
Submit,
|
||||
TextInput
|
||||
}
|
||||
|
||||
alias AshHqWeb.Router.Helpers, as: Routes
|
||||
|
||||
prop params, :map
|
||||
|
||||
data error, :boolean, default: false
|
||||
data password_reset_form, :any
|
||||
|
||||
def render(assigns) do
|
||||
~F"""
|
||||
<div class="container flex flex-wrap mx-auto">
|
||||
<div class="w-full md:w-2/3 ml-2">
|
||||
{#if @password_reset_form}
|
||||
<Form class="space-y-8" for={@password_reset_form} submit="reset_password">
|
||||
<div class="space-y-8 sm:space-y-5">
|
||||
<div>
|
||||
<h3 class="text-lg leading-6 font-medium">Reset Password</h3>
|
||||
{#if @password_reset_form.submitted_once?}
|
||||
<div class="alert alert-danger">
|
||||
<p>Oops, something went wrong! Please check the errors below.</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-6 sm:mt-5 space-y-6 sm:space-y-5">
|
||||
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5">
|
||||
<Field name={:password}>
|
||||
<Label class="block text-sm font-medium sm:mt-px sm:pt-2">Password</Label>
|
||||
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<div class="max-w-lg flex rounded-md shadow-sm">
|
||||
<PasswordInput class="flex-1 text-black block w-full focus:ring-primary-light-600 focus:border-primary-light-600 min-w-0 rounded-md sm:text-sm border-base-light-300" />
|
||||
</div>
|
||||
<ErrorTag />
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 sm:mt-5 space-y-6 sm:space-y-5">
|
||||
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5">
|
||||
<Field name={:password_confirmation}>
|
||||
<Label class="block text-sm font-medium sm:mt-px sm:pt-2">Password Confirmation</Label>
|
||||
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<div class="max-w-lg flex rounded-md shadow-sm">
|
||||
<PasswordInput class="flex-1 text-black block w-full focus:ring-primary-light-600 focus:border-primary-light-600 min-w-0 rounded-md sm:text-sm border-base-light-300" />
|
||||
</div>
|
||||
<ErrorTag />
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-5">
|
||||
<div class="flex justify-end">
|
||||
<Submit class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md bg-primary-light-600 hover:bg-primary-light-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-light-600 text-white">Reset Password</Submit>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
{#else}
|
||||
<Form class="space-y-8" for={:request_password_reset} submit="request_password_reset">
|
||||
<div class="space-y-8 sm:space-y-5">
|
||||
<div>
|
||||
<h3 class="text-lg leading-6 font-medium">Choose New Password</h3>
|
||||
{#if @error}
|
||||
<div class="alert alert-danger">
|
||||
<p>Oops, something went wrong! Please check the errors below.</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-6 sm:mt-5 space-y-6 sm:space-y-5">
|
||||
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:pt-5">
|
||||
<Field name={:email}>
|
||||
<Label class="block text-sm font-medium sm:mt-px sm:pt-2">Email</Label>
|
||||
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
||||
<div class="max-w-lg flex rounded-md shadow-sm">
|
||||
<TextInput
|
||||
class="flex-1 text-black block w-full focus:ring-primary-light-600 focus:border-primary-light-600 min-w-0 rounded-md sm:text-sm border-base-light-300"
|
||||
opts={autocomplete: "email"}
|
||||
/>
|
||||
</div>
|
||||
<ErrorTag />
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-5">
|
||||
<div class="flex justify-end">
|
||||
<Submit class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md bg-primary-light-600 hover:bg-primary-light-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-light-600 text-white">Send Password Reset Email</Submit>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
<LivePatch to={Routes.app_view_path(AshHqWeb.Endpoint, :log_in)}>Remember your password?</LivePatch>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def update(assigns, socket) do
|
||||
{:ok, socket |> assign(assigns) |> assign_reset_form()}
|
||||
end
|
||||
|
||||
defp assign_reset_form(socket) do
|
||||
case socket.assigns[:params]["token"] do
|
||||
nil ->
|
||||
assign(socket, password_reset_form: nil)
|
||||
|
||||
token ->
|
||||
user =
|
||||
AshHq.Accounts.User
|
||||
|> Ash.Query.for_read(
|
||||
:with_verified_email_token,
|
||||
%{token: token, context: "reset_password"},
|
||||
authorize?: false
|
||||
)
|
||||
|> AshHq.Accounts.read_one!()
|
||||
|
||||
if user do
|
||||
assign(
|
||||
socket,
|
||||
:password_reset_form,
|
||||
AshPhoenix.Form.for_update(user, :reset_password,
|
||||
as: "reset_password",
|
||||
api: AshHq.Accounts,
|
||||
authorize?: false
|
||||
)
|
||||
)
|
||||
else
|
||||
socket
|
||||
|> put_flash(:error, "Reset password link is invalid or it has expired.")
|
||||
|> assign(password_reset_form: nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("reset_password", %{"reset_password" => params}, socket) do
|
||||
case AshPhoenix.Form.submit(socket.assigns.password_reset_form,
|
||||
params: params
|
||||
) do
|
||||
{:ok, _result} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Successfully reset password.")
|
||||
|> push_redirect(to: Routes.app_view_path(AshHqWeb.Endpoint, :log_in))}
|
||||
|
||||
{:error, form} ->
|
||||
{:noreply, assign(socket, password_reset_form: form)}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(
|
||||
"request_password_reset",
|
||||
%{"request_password_reset" => %{"email" => email}},
|
||||
socket
|
||||
) do
|
||||
with {:ok, user} <-
|
||||
AshHq.Accounts.get(AshHq.Accounts.User, [email: String.trim(email)], authorize?: false) do
|
||||
user
|
||||
|> Ash.Changeset.for_update(
|
||||
:deliver_user_reset_password_instructions,
|
||||
%{reset_password_url_fun: &Routes.app_view_url(AshHqWeb.Endpoint, :reset_password, &1)},
|
||||
authorize?: false
|
||||
)
|
||||
|> AshHq.Accounts.update!()
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(
|
||||
:info,
|
||||
"If your email is in our system, you will receive instructions to reset your password shortly."
|
||||
)
|
||||
|> push_redirect(to: Routes.app_view_path(AshHqWeb.Endpoint, :reset_password))}
|
||||
end
|
||||
end
|
|
@ -2,7 +2,6 @@ defmodule AshHqWeb.Pages.UserSettings do
|
|||
@moduledoc "User settings page"
|
||||
use Surface.LiveComponent
|
||||
|
||||
alias AshHqWeb.Router.Helpers, as: Routes
|
||||
alias Surface.Components.Form
|
||||
|
||||
alias Surface.Components.Form.{
|
||||
|
@ -15,13 +14,13 @@ defmodule AshHqWeb.Pages.UserSettings do
|
|||
TextInput
|
||||
}
|
||||
|
||||
prop current_user, :map, required: true
|
||||
prop(current_user, :map, required: true)
|
||||
|
||||
data email_form, :map
|
||||
data password_form, :map
|
||||
data merch_form, :map
|
||||
data address, :string
|
||||
data name, :string
|
||||
data(email_form, :map)
|
||||
data(password_form, :map)
|
||||
data(merch_form, :map)
|
||||
data(address, :string)
|
||||
data(name, :string)
|
||||
|
||||
def render(assigns) do
|
||||
~F"""
|
||||
|
@ -255,7 +254,7 @@ defmodule AshHqWeb.Pages.UserSettings do
|
|||
email_form:
|
||||
AshPhoenix.Form.for_update(
|
||||
socket.assigns.current_user,
|
||||
:deliver_update_email_instructions,
|
||||
:update_email,
|
||||
as: "update_email",
|
||||
api: AshHq.Accounts,
|
||||
actor: socket.assigns.current_user
|
||||
|
@ -289,13 +288,6 @@ defmodule AshHqWeb.Pages.UserSettings do
|
|||
|
||||
@impl true
|
||||
def handle_event("save_update_email", %{"update_email" => params}, socket) do
|
||||
params =
|
||||
Map.put(
|
||||
params,
|
||||
"update_url_fun",
|
||||
&Routes.user_settings_url(AshHqWeb.Endpoint, :confirm_email, &1)
|
||||
)
|
||||
|
||||
case AshPhoenix.Form.submit(socket.assigns.email_form, params: params) do
|
||||
{:ok, _result} ->
|
||||
{:noreply,
|
||||
|
@ -359,15 +351,9 @@ defmodule AshHqWeb.Pages.UserSettings do
|
|||
|
||||
@impl true
|
||||
def handle_event("resend_confirmation_instructions", _, socket) do
|
||||
socket.assigns.current_user
|
||||
|> Ash.Changeset.for_update(
|
||||
:deliver_user_confirmation_instructions,
|
||||
%{
|
||||
confirmation_url_fun: &Routes.user_confirmation_url(AshHqWeb.Endpoint, :confirm, &1)
|
||||
},
|
||||
AshHq.Accounts.User.resend_confirmation_instructions!(socket.assigns.current_user,
|
||||
actor: socket.assigns.current_user
|
||||
)
|
||||
|> AshHq.Accounts.update!()
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
defmodule AshHqWeb.Router do
|
||||
use AshHqWeb, :router
|
||||
use AshAuthentication.Phoenix.Router
|
||||
|
||||
import AshHqWeb.UserAuth
|
||||
import AshAdmin.Router
|
||||
|
||||
pipeline :browser do
|
||||
|
@ -12,25 +12,38 @@ defmodule AshHqWeb.Router do
|
|||
plug :protect_from_forgery
|
||||
plug AshHqWeb.SessionPlug
|
||||
plug :assign_user_agent
|
||||
end
|
||||
|
||||
pipeline :dead_view_authentication do
|
||||
plug :fetch_current_user
|
||||
plug :load_from_session
|
||||
end
|
||||
|
||||
pipeline :api do
|
||||
plug :accepts, ["json"]
|
||||
plug :load_from_bearer
|
||||
end
|
||||
|
||||
pipeline :admin_basic_auth do
|
||||
plug :basic_auth
|
||||
end
|
||||
|
||||
scope "/", AshHqWeb do
|
||||
pipe_through :browser
|
||||
reset_route []
|
||||
|
||||
sign_in_route overrides: [AshHqWeb.AuthOverrides, AshAuthentication.Phoenix.Overrides.Default]
|
||||
|
||||
sign_out_route AuthController
|
||||
auth_routes_for(AshHq.Accounts.User, to: AuthController)
|
||||
end
|
||||
|
||||
scope "/", AshHqWeb do
|
||||
pipe_through :browser
|
||||
|
||||
live_session :main,
|
||||
on_mount: [{AshHqWeb.InitAssigns, :default}, {AshHqWeb.LiveUserAuth, :live_user}],
|
||||
on_mount: [
|
||||
AshAuthentication.Phoenix.LiveSession,
|
||||
{AshHqWeb.LiveUserAuth, :live_user_optional},
|
||||
{AshHqWeb.InitAssigns, :default}
|
||||
],
|
||||
session: {AshAuthentication.Phoenix.LiveSession, :generate_session, []},
|
||||
root_layout: {AshHqWeb.LayoutView, :root} do
|
||||
live "/", AppViewLive, :home
|
||||
live "/media", AppViewLive, :media
|
||||
|
@ -49,20 +62,13 @@ defmodule AshHqWeb.Router do
|
|||
get "/unsubscribe", MailingListController, :unsubscribe
|
||||
end
|
||||
|
||||
live_session :unauthenticated_only,
|
||||
on_mount: [
|
||||
{AshHqWeb.InitAssigns, :default},
|
||||
{AshHqWeb.LiveUserAuth, :live_user_not_allowed}
|
||||
],
|
||||
root_layout: {AshHqWeb.LayoutView, :root} do
|
||||
live "/users/log_in", AppViewLive, :log_in
|
||||
live "/users/register", AppViewLive, :register
|
||||
live "/users/reset_password", AppViewLive, :reset_password
|
||||
live "/users/reset_password/:token", AppViewLive, :reset_password
|
||||
end
|
||||
|
||||
live_session :authenticated_only,
|
||||
on_mount: [{AshHqWeb.InitAssigns, :default}, {AshHqWeb.LiveUserAuth, :live_user_required}],
|
||||
on_mount: [
|
||||
AshAuthentication.Phoenix.LiveSession,
|
||||
{AshHqWeb.InitAssigns, :default},
|
||||
{AshHqWeb.LiveUserAuth, :live_user_required}
|
||||
],
|
||||
session: {AshAuthentication.Phoenix.LiveSession, :generate_session, []},
|
||||
root_layout: {AshHqWeb.LayoutView, :root} do
|
||||
live "/users/settings", AppViewLive, :user_settings
|
||||
end
|
||||
|
@ -80,34 +86,6 @@ defmodule AshHqWeb.Router do
|
|||
interface: :playground
|
||||
end
|
||||
|
||||
## Authentication routes
|
||||
|
||||
scope "/", AshHqWeb do
|
||||
pipe_through [
|
||||
:browser,
|
||||
:dead_view_authentication,
|
||||
:redirect_if_user_is_authenticated,
|
||||
:put_session_layout
|
||||
]
|
||||
|
||||
get "/users/new_session", UserSessionController, :log_in
|
||||
post "/users/new_session", UserSessionController, :log_in
|
||||
end
|
||||
|
||||
scope "/", AshHqWeb do
|
||||
pipe_through [:browser, :dead_view_authentication, :require_authenticated_user]
|
||||
|
||||
get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
|
||||
end
|
||||
|
||||
scope "/", AshHqWeb do
|
||||
pipe_through [:browser, :dead_view_authentication]
|
||||
|
||||
post "/users/log_out", UserSessionController, :delete
|
||||
post "/users/confirm", UserConfirmationController, :create
|
||||
get "/users/confirm/:token", UserConfirmationController, :confirm
|
||||
end
|
||||
|
||||
# Enables LiveDashboard only for development
|
||||
#
|
||||
# If you want to use the LiveDashboard in production, you should put
|
||||
|
@ -119,7 +97,7 @@ defmodule AshHqWeb.Router do
|
|||
|
||||
scope "/" do
|
||||
if Mix.env() in [:dev, :test] do
|
||||
pipe_through [:browser, :dead_view_authentication]
|
||||
pipe_through [:browser]
|
||||
else
|
||||
pipe_through [:browser, :admin_basic_auth]
|
||||
end
|
||||
|
@ -138,7 +116,7 @@ defmodule AshHqWeb.Router do
|
|||
# node running the Phoenix server.
|
||||
if Mix.env() == :dev do
|
||||
scope "/dev" do
|
||||
pipe_through [:browser, :dead_view_authentication]
|
||||
pipe_through [:browser]
|
||||
|
||||
forward "/mailbox", Plug.Swoosh.MailboxPreview
|
||||
end
|
||||
|
|
6
lib/ash_hq_web/routes.ex
Normal file
6
lib/ash_hq_web/routes.ex
Normal file
|
@ -0,0 +1,6 @@
|
|||
defmodule AshHqWeb.Routes do
|
||||
@moduledoc "Route helpers"
|
||||
def url(path) do
|
||||
Application.get_env(:ash_hq, :url) <> "/" <> String.trim_leading(path, "/")
|
||||
end
|
||||
end
|
|
@ -38,6 +38,21 @@
|
|||
</script>
|
||||
</head>
|
||||
<body class="h-full">
|
||||
<%= case live_flash(@flash, :info) do %>
|
||||
<% nil -> %>
|
||||
<% flash -> %>
|
||||
<p class="alert alert-info" role="alert" phx-click="lv:clear-flash" phx-value-key="info">
|
||||
<%= flash %>
|
||||
</p>
|
||||
<% end %>
|
||||
<%= case live_flash(@flash, :error) do %>
|
||||
<% nil -> %>
|
||||
<% flash -> %>
|
||||
<p class="alert alert-danger" role="alert" phx-click="lv:clear-flash" phx-value-key="error">
|
||||
<%= flash %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<%= @inner_content %>
|
||||
<%= if @live_action == :docs do %>
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<%= csrf_meta_tag() %>
|
||||
<%= live_title_tag assigns[:page_title] || "AshHq" %>
|
||||
<link phx-track-static rel="stylesheet" href="<%= Routes.static_path(@conn, "/assets/app.css") %>"/>
|
||||
<script defer phx-track-static type="text/javascript" src="<%= Routes.static_path(@conn, "/assets/app.js") %>"></script>
|
||||
</head>
|
||||
<body class="flex flex-col h-full bg-base-light-700">
|
||||
<header>
|
||||
<nav class="flex flex-wrap items-center justify-between py-2 px-4 fixed top-0 right-0 left-0 z-30 bg-base-light-700">
|
||||
<a class="inline-block mr-4 py-1 text-xl text-base-light-300" href="#"><i class="fas fa-bolt"></i> AshHq</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main role="main" class="container mx-auto mb-8 px-4 max-w-6xl">
|
||||
<div class="flex justify-center p-2">
|
||||
<div class="card w-full max-w-xs">
|
||||
<div class="card-body">
|
||||
<%= @inner_content %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -1,199 +0,0 @@
|
|||
defmodule AshHqWeb.UserAuth do
|
||||
@moduledoc """
|
||||
Helpers for authenticating, logging in and logging out users.
|
||||
"""
|
||||
|
||||
import Plug.Conn
|
||||
import Phoenix.Controller
|
||||
|
||||
alias AshHq.Accounts
|
||||
alias AshHqWeb.Router.Helpers, as: Routes
|
||||
require Ash.Query
|
||||
|
||||
# Make the remember me cookie valid for 60 days.
|
||||
# If you want bump or reduce this value, also change
|
||||
# the token expiry itself in UserToken.
|
||||
@max_age 60 * 60 * 24 * 60
|
||||
@remember_me_cookie "_reference_live_app_web_user_remember_me"
|
||||
@remember_me_options [sign: true, max_age: @max_age, same_site: "Lax"]
|
||||
|
||||
@doc """
|
||||
Logs the user in.
|
||||
|
||||
It renews the session ID and clears the whole session
|
||||
to avoid fixation attacks. See the renew_session
|
||||
function to customize this behaviour.
|
||||
|
||||
It also sets a `:live_socket_id` key in the session,
|
||||
so LiveView sessions are identified and automatically
|
||||
disconnected on log out. The line can be safely removed
|
||||
if you are not using LiveView.
|
||||
"""
|
||||
def log_in_user(conn, user, params \\ %{}) do
|
||||
token = create_token_for_user(user)
|
||||
|
||||
log_in_with_token(conn, token, params["remember_me"] == "true")
|
||||
end
|
||||
|
||||
def log_in_with_token(conn, token, remember_me?, return_to \\ nil) do
|
||||
user_return_to = return_to || get_session(conn, :user_return_to) || "/"
|
||||
|
||||
conn
|
||||
|> renew_session()
|
||||
|> put_session(:user_token, token)
|
||||
|> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}")
|
||||
|> maybe_write_remember_me_cookie(token, remember_me?)
|
||||
|> redirect(to: user_return_to || signed_in_path(conn))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a token for a user, effectively "logging them in".
|
||||
|
||||
This is used in liveviews, after which the token is sent to
|
||||
the session creation endpoint, which stores the token in the session.
|
||||
"""
|
||||
def create_token_for_user(user) do
|
||||
Accounts.UserToken
|
||||
|> Ash.Changeset.for_create(:build_session_token, %{user: user.id}, authorize?: false)
|
||||
|> Accounts.create!()
|
||||
|> Map.get(:token)
|
||||
end
|
||||
|
||||
defp maybe_write_remember_me_cookie(conn, token, true) do
|
||||
put_resp_cookie(conn, @remember_me_cookie, token, @remember_me_options)
|
||||
end
|
||||
|
||||
defp maybe_write_remember_me_cookie(conn, _token, _params) do
|
||||
conn
|
||||
end
|
||||
|
||||
# This function renews the session ID and erases the whole
|
||||
# session to avoid fixation attacks. If there is any data
|
||||
# in the session you may want to preserve after log in/log out,
|
||||
# you must explicitly fetch the session data before clearing
|
||||
# and then immediately set it after clearing, for example:
|
||||
#
|
||||
# defp renew_session(conn) do
|
||||
# preferred_locale = get_session(conn, :preferred_locale)
|
||||
#
|
||||
# conn
|
||||
# |> configure_session(renew: true)
|
||||
# |> clear_session()
|
||||
# |> put_session(:preferred_locale, preferred_locale)
|
||||
# end
|
||||
#
|
||||
defp renew_session(conn) do
|
||||
conn
|
||||
|> configure_session(renew: true)
|
||||
|> clear_session()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Logs the user out.
|
||||
|
||||
It clears all session data for safety. See renew_session.
|
||||
"""
|
||||
def log_out_user(conn) do
|
||||
user_token = get_session(conn, :user_token)
|
||||
|
||||
if user_token do
|
||||
{:ok, query} =
|
||||
AshHq.Accounts.UserToken
|
||||
|> Ash.Query.filter(token == ^user_token and context == "session")
|
||||
|> Ash.Query.data_layer_query()
|
||||
|
||||
AshHq.Repo.delete_all(query)
|
||||
end
|
||||
|
||||
if live_socket_id = get_session(conn, :live_socket_id) do
|
||||
AshHqWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
|
||||
end
|
||||
|
||||
conn
|
||||
|> renew_session()
|
||||
|> delete_resp_cookie(@remember_me_cookie)
|
||||
|> redirect(to: Routes.app_view_path(AshHqWeb.Endpoint, :home))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Authenticates the user by looking into the session
|
||||
and remember me token.
|
||||
"""
|
||||
def fetch_current_user(conn, _opts) do
|
||||
{user_token, conn} = ensure_user_token(conn)
|
||||
|
||||
assign(conn, :current_user, user_for_session_token(user_token))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the user corresponding to a given session token.
|
||||
|
||||
If the session token is nil or does not exist, then `nil` is returned.
|
||||
"""
|
||||
def user_for_session_token(nil), do: nil
|
||||
|
||||
def user_for_session_token(user_token) do
|
||||
AshHq.Accounts.User
|
||||
|> Ash.Query.for_read(:by_token, %{token: user_token, context: "session"}, authorize?: false)
|
||||
|> AshHq.Accounts.read_one!()
|
||||
end
|
||||
|
||||
defp ensure_user_token(conn) do
|
||||
if user_token = get_session(conn, :user_token) do
|
||||
{user_token, conn}
|
||||
else
|
||||
conn = fetch_cookies(conn, signed: [@remember_me_cookie])
|
||||
|
||||
if user_token = conn.cookies[@remember_me_cookie] do
|
||||
{user_token, put_session(conn, :user_token, user_token)}
|
||||
else
|
||||
{nil, conn}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Used for routes that require the user to not be authenticated.
|
||||
"""
|
||||
def redirect_if_user_is_authenticated(conn, _opts) do
|
||||
if conn.assigns[:current_user] do
|
||||
conn
|
||||
|> redirect(to: signed_in_path(conn))
|
||||
|> halt()
|
||||
else
|
||||
conn
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Used for routes that require the user to be authenticated.
|
||||
|
||||
If you want to enforce the user email is confirmed before
|
||||
they use the application at all, here would be a good place.
|
||||
"""
|
||||
def require_authenticated_user(conn, _opts) do
|
||||
if conn.assigns[:current_user] do
|
||||
conn
|
||||
else
|
||||
conn
|
||||
|> put_flash(:error, "You must log in to access this page.")
|
||||
|> maybe_store_return_to()
|
||||
|> redirect(to: Routes.app_view_path(conn, :log_in))
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_store_return_to(%{method: "GET"} = conn) do
|
||||
put_session(conn, :user_return_to, current_path(conn))
|
||||
end
|
||||
|
||||
defp maybe_store_return_to(conn), do: conn
|
||||
|
||||
defp signed_in_path(_conn), do: "/"
|
||||
|
||||
def put_session_layout(conn, _opts) do
|
||||
conn
|
||||
|> put_layout(false)
|
||||
|> put_root_layout({AshHqWeb.LayoutView, :session})
|
||||
end
|
||||
end
|
|
@ -5,7 +5,7 @@ defmodule AshHqWeb.AppViewLive do
|
|||
|
||||
alias AshHqWeb.Components.AppView.TopBar
|
||||
alias AshHqWeb.Components.{CatalogueModal, Search}
|
||||
alias AshHqWeb.Pages.{Blog, Docs, Home, LogIn, Media, Register, ResetPassword, UserSettings}
|
||||
alias AshHqWeb.Pages.{Blog, Docs, Home, Media, UserSettings}
|
||||
alias Phoenix.LiveView.JS
|
||||
alias Surface.Components.Context
|
||||
require Ash.Query
|
||||
|
@ -118,12 +118,6 @@ defmodule AshHqWeb.AppViewLive do
|
|||
/>
|
||||
{#match :user_settings}
|
||||
<UserSettings id="user_settings" current_user={@current_user} />
|
||||
{#match :log_in}
|
||||
<LogIn id="log_in" />
|
||||
{#match :register}
|
||||
<Register id="register" />
|
||||
{#match :reset_password}
|
||||
<ResetPassword id="reset_password" params={@params} />
|
||||
{#match :media}
|
||||
<Media id="media" />
|
||||
{/case}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
defmodule AshHqWeb.UserConfirmationView do
|
||||
use AshHqWeb, :view
|
||||
end
|
|
@ -1,3 +0,0 @@
|
|||
defmodule AshHqWeb.UserResetPasswordView do
|
||||
use AshHqWeb, :view
|
||||
end
|
|
@ -1,3 +0,0 @@
|
|||
defmodule AshHqWeb.UserSessionView do
|
||||
use AshHqWeb, :view
|
||||
end
|
|
@ -1,3 +0,0 @@
|
|||
defmodule AshHqWeb.UserSettingsView do
|
||||
use AshHqWeb, :view
|
||||
end
|
4
mix.exs
4
mix.exs
|
@ -45,6 +45,8 @@ defmodule AshHq.MixProject do
|
|||
{:ash_phoenix, github: "ash-project/ash_phoenix", override: true},
|
||||
{:ash_graphql, github: "ash-project/ash_graphql"},
|
||||
{:ash_json_api, github: "ash-project/ash_json_api"},
|
||||
{:ash_authentication, github: "team-alembic/ash_authentication", override: true},
|
||||
{:ash_authentication_phoenix, github: "team-alembic/ash_authentication_phoenix"},
|
||||
{:absinthe_plug, "~> 1.5"},
|
||||
{:ash_blog, github: "ash-project/ash_blog"},
|
||||
{:ash_csv, github: "ash-project/ash_csv"},
|
||||
|
@ -79,7 +81,7 @@ defmodule AshHq.MixProject do
|
|||
{:ecto_psql_extras, "~> 0.6"},
|
||||
{:phoenix_ecto, "~> 4.4"},
|
||||
# Phoenix/Core dependencies
|
||||
{:phoenix, "~> 1.7.0-rc.0", override: true},
|
||||
{:phoenix, "~> 1.7.0-rc.1", override: true},
|
||||
{:phoenix_view, "~> 2.0"},
|
||||
{:ecto_sql, "~> 3.6"},
|
||||
{:postgrex, ">= 0.0.0"},
|
||||
|
|
18
mix.lock
18
mix.lock
|
@ -1,17 +1,20 @@
|
|||
%{
|
||||
"absinthe": {:hex, :absinthe, "1.7.0", "36819e7b1fd5046c9c734f27fe7e564aed3bda59f0354c37cd2df88fd32dd014", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0 or ~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "566a5b5519afc9b29c4d367f0c6768162de3ec03e9bf9916f9dc2bcbe7c09643"},
|
||||
"absinthe_plug": {:hex, :absinthe_plug, "1.5.8", "38d230641ba9dca8f72f1fed2dfc8abd53b3907d1996363da32434ab6ee5d6ab", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bbb04176647b735828861e7b2705465e53e2cf54ccf5a73ddd1ebd855f996e5a"},
|
||||
"ash": {:git, "https://github.com/ash-project/ash.git", "1ccec692cb76f76bc33164dedbf9ee14ed8a8359", []},
|
||||
"ash": {:git, "https://github.com/ash-project/ash.git", "1eaeacc7486e50d70138baaf65133cef5e8b5869", []},
|
||||
"ash_admin": {:git, "https://github.com/ash-project/ash_admin.git", "cdb3b469abeeadddda884e7cfcf67c6fea10f9ef", []},
|
||||
"ash_authentication": {:git, "https://github.com/team-alembic/ash_authentication.git", "161c8ab7e8a9bb38955b6c1872246e065327ce62", []},
|
||||
"ash_authentication_phoenix": {:git, "https://github.com/team-alembic/ash_authentication_phoenix.git", "fbe5272f874532b3119033ddb1dfb49d71f7b571", []},
|
||||
"ash_blog": {:git, "https://github.com/ash-project/ash_blog.git", "9254773dfedabfc7987af6326a62885c24c3655b", []},
|
||||
"ash_csv": {:git, "https://github.com/ash-project/ash_csv.git", "77187f6e4505ed4d88598bf87e56983a6a74a456", []},
|
||||
"ash_graphql": {:git, "https://github.com/ash-project/ash_graphql.git", "09fe8ce9bbbdf03d12b17e5037235a7752326e59", []},
|
||||
"ash_json_api": {:git, "https://github.com/ash-project/ash_json_api.git", "5594f18d07d0771b4bc8b3d3db99cc8c08958a66", []},
|
||||
"ash_phoenix": {:git, "https://github.com/ash-project/ash_phoenix.git", "b205c9bb4f153f855420f11d766cfe515930631e", []},
|
||||
"ash_postgres": {:hex, :ash_postgres, "1.2.2", "0da0d9e1878f8b24829a8971e497e7bafe5596aced3604ba39d96e70174a8b11", [:mix], [{:ash, ">= 2.4.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.5", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "8796f1de22863451a445f33a0874904145efeecfff75175f6c1ce62754c8df4e"},
|
||||
"assent": {:hex, :assent, "0.2.1", "46ad0ed92b72330f38c60bc03c528e8408475dc386f48d4ecd18833cfa581b9f", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: true]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "58c558b6029ffa287e15b38c8e07cd99f0b24e4846c52abad0c0a6225c4873bc"},
|
||||
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.0.1", "9be815469e6bfefec40fa74658ecbbe6897acfb57614df1416eeccd4903f602c", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "486bb95efb645d1efc6794c1ddd776a186a9a713abf06f45708a6ce324fb96cf"},
|
||||
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
|
||||
"castore": {:hex, :castore, "0.1.20", "62a0126cbb7cb3e259257827b9190f88316eb7aa3fdac01fd6f2dfd64e7f46e9", [:mix], [], "hexpm", "a020b7650529c986c454a4035b6b13a328e288466986307bea3aadb4c95ac98a"},
|
||||
"castore": {:hex, :castore, "0.1.22", "4127549e411bedd012ca3a308dede574f43819fe9394254ca55ab4895abfa1a2", [:mix], [], "hexpm", "c17576df47eb5aa1ee40cc4134316a99f5cad3e215d5c77b8dd3cfef12a22cac"},
|
||||
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
|
||||
"cloak": {:hex, :cloak, "1.1.2", "7e0006c2b0b98d976d4f559080fabefd81f0e0a50a3c4b621f85ceeb563e80bb", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "940d5ac4fcd51b252930fd112e319ea5ae6ab540b722f3ca60a85666759b9585"},
|
||||
"comeonin": {:hex, :comeonin, "5.3.3", "2c564dac95a35650e9b6acfe6d2952083d8a08e4a89b93a481acb552b325892e", [:mix], [], "hexpm", "3e38c9c2cb080828116597ca8807bb482618a315bfafd98c90bc22a821cc84df"},
|
||||
|
@ -52,6 +55,8 @@
|
|||
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
|
||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||
"jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
|
||||
"joken": {:hex, :joken, "2.5.0", "09be497d804b8115eb6f07615cef2e60c2a1008fb89dc0aef0d4c4b4609b99aa", [:mix], [{:jose, "~> 1.11.2", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "22b25c89617c5ed8ca7b31026340a25ea0f9ca7160f9706b79be9ed81fdf74e7"},
|
||||
"jose": {:hex, :jose, "1.11.5", "3bc2d75ffa5e2c941ca93e5696b54978323191988eb8d225c2e663ddfefd515e", [:mix, :rebar3], [], "hexpm", "dcd3b215bafe02ea7c5b23dafd3eb8062a5cd8f2d904fd9caa323d37034ab384"},
|
||||
"json_xema": {:hex, :json_xema, "0.4.2", "85de190f597a98ce9da436b8a59c97ef561a6ab6017255df8b494babefd6fb10", [:mix], [{:conv_case, "~> 0.2", [hex: :conv_case, repo: "hexpm", optional: false]}, {:xema, "~> 0.11", [hex: :xema, repo: "hexpm", optional: false]}], "hexpm", "5516213758667d21669e0d63ea287238d277519527bac6c02140a5e34c1fda80"},
|
||||
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
|
||||
"makeup_eex": {:hex, :makeup_eex, "0.1.1", "89352d5da318d97ae27bbcc87201f274504d2b71ede58ca366af6a5fbed9508d", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.16", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_html, "~> 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d111a0994eaaab09ef1a4b3b313ef806513bb4652152c26c0d7ca2be8402a964"},
|
||||
|
@ -70,12 +75,12 @@
|
|||
"nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"},
|
||||
"parallel_stream": {:hex, :parallel_stream, "1.1.0", "f52f73eb344bc22de335992377413138405796e0d0ad99d995d9977ac29f1ca9", [:mix], [], "hexpm", "684fd19191aedfaf387bbabbeb8ff3c752f0220c8112eb907d797f4592d6e871"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
|
||||
"phoenix": {:hex, :phoenix, "1.7.0-rc.0", "8e328572f496b5170e879da94baa57c5f878f354d50eac052c9a7c6d57c2cf54", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.4", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "ed503f6c55184afc0a453e44e6ab2a09f014f59b7fdd682313fdc52ec2f82859"},
|
||||
"phoenix": {:hex, :phoenix, "1.7.0-rc.1", "28d6591441347ba68da9750771cec6fe18ce040c91095a46d5d332804d5037d5", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.4", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "96d98dcf161b2784fd08a52fd480729a9eeae33773440b4e7a89d1e7e804af52"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"},
|
||||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"},
|
||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.18.3", "2e3d009422addf8b15c3dccc65ce53baccbe26f7cfd21d264680b5867789a9c1", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c8845177a866e017dcb7083365393c8f00ab061b8b6b2bda575891079dce81b2"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.18.6", "460c36977643d76fc8e0b6b3c4bba703c0ef21abc74233cf7dc15d1c1696832f", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce2768fb44c3c370df13fc4f0dc70623b662a93a201d8d7d87c4ba6542bc6b73"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
|
||||
"phoenix_template": {:hex, :phoenix_template, "1.0.0", "c57bc5044f25f007dc86ab21895688c098a9f846a8dda6bc40e2d0ddc146e38f", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "1b066f99a26fd22064c12b2600a9a6e56700f591bf7b20b418054ea38b4d4357"},
|
||||
"phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"},
|
||||
|
@ -87,9 +92,10 @@
|
|||
"postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"},
|
||||
"premailex": {:hex, :premailex, "0.3.16", "25c0c9c969f0025bbfdb06834f8f0fbd46e5ec50f5c252e6492165802ffbd2a6", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:floki, "~> 0.19", [hex: :floki, repo: "hexpm", optional: false]}, {:meeseeks, "~> 0.11", [hex: :meeseeks, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "c6b042f89ca63025dfbe3ef54fdbbe9d5f043b7c33d8e58f43a41d13a9475111"},
|
||||
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
||||
"slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"},
|
||||
"sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"},
|
||||
"sourceror": {:hex, :sourceror, "0.11.2", "549ce48be666421ac60cfb7f59c8752e0d393baa0b14d06271d3f6a8c1b027ab", [:mix], [], "hexpm", "9ab659118896a36be6eec68ff7b0674cba372fc8e210b1e9dc8cf2b55bb70dfb"},
|
||||
"spark": {:hex, :spark, "0.3.1", "87b5bf790bfa99b4b8e748fe0b6c8bf0db29d73c861d346d072a1f1083e73cb3", [:mix], [{:nimble_options, "~> 0.5", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "425fa3f404d377cac4493c181142443daf778988d43c6319e1e3435d3af515df"},
|
||||
"spark": {:hex, :spark, "0.3.4", "0084ce931c0e444194d5198b6f872c74c85607b6e5672436056e9f5b6fa41139", [:mix], [{:nimble_options, "~> 0.5", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "7816d3a43916c5fac2bb2308f9a3442644950e5e269014e4cdc5b2cab0bab5e0"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
|
||||
"stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"},
|
||||
"sunflower_ui": {:git, "https://github.com/zachdaniel/sunflower_ui.git", "3ec87f33e003693e6db2329f9d6d8ac59983cf17", []},
|
||||
|
@ -98,7 +104,7 @@
|
|||
"swoosh": {:hex, :swoosh, "1.8.1", "b1694d57c01852f50f7d4e6a74f5d119b2d8722d6a82f7288703c3e448ddbbf8", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e64a93d71d1e1e32db681cf7870697495c9cb2df4a5484f4d91ded326ccd3cbb"},
|
||||
"table_rex": {:hex, :table_rex, "3.1.1", "0c67164d1714b5e806d5067c1e96ff098ba7ae79413cc075973e17c38a587caa", [:mix], [], "hexpm", "678a23aba4d670419c23c17790f9dcd635a4a89022040df7d5d772cb21012490"},
|
||||
"tails": {:hex, :tails, "0.1.1", "4d912b8c4e5bf244f2e899fee76b2d2fb99d2c4740defaa00ed5570266ee87f4", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "4d273173487f32db0040d901a312a6bf347ba5159b6c2fdf080830649839569c"},
|
||||
"telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"},
|
||||
"telemetry": {:hex, :telemetry, "1.2.0", "a8ce551485a9a3dac8d523542de130eafd12e40bbf76cf0ecd2528f24e812a44", [:rebar3], [], "hexpm", "1427e73667b9a2002cf1f26694c422d5c905df889023903c4518921d53e3e883"},
|
||||
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
|
||||
"telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
|
||||
"typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"},
|
||||
|
|
51
priv/repo/migrations/20230110202444_migrate_resources36.exs
Normal file
51
priv/repo/migrations/20230110202444_migrate_resources36.exs
Normal file
|
@ -0,0 +1,51 @@
|
|||
defmodule AshHq.Repo.Migrations.MigrateResources36 do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
alter table(:users) do
|
||||
modify(:hashed_password, :text, null: false)
|
||||
end
|
||||
|
||||
alter table(:user_tokens) do
|
||||
remove(:sent_to)
|
||||
remove(:context)
|
||||
remove(:token)
|
||||
remove(:id)
|
||||
|
||||
add(:updated_at, :utc_datetime_usec, null: false, default: fragment("now()"))
|
||||
add(:extra_data, :map)
|
||||
add(:purpose, :text, null: false)
|
||||
add(:expires_at, :utc_datetime, null: false)
|
||||
add(:jti, :text, null: false, primary_key: true)
|
||||
end
|
||||
|
||||
drop_if_exists(
|
||||
unique_index(:user_tokens, [:context, :token], name: "user_tokens_token_context_index")
|
||||
)
|
||||
end
|
||||
|
||||
def down do
|
||||
create unique_index(:user_tokens, [:context, :token], name: "user_tokens_token_context_index")
|
||||
|
||||
alter table(:user_tokens) do
|
||||
remove(:jti)
|
||||
remove(:expires_at)
|
||||
remove(:purpose)
|
||||
remove(:extra_data)
|
||||
remove(:updated_at)
|
||||
add(:id, :uuid, null: false, default: fragment("uuid_generate_v4()"), primary_key: true)
|
||||
add(:context, :text)
|
||||
add(:sent_to, :text)
|
||||
end
|
||||
|
||||
alter table(:users) do
|
||||
modify(:hashed_password, :text, null: true)
|
||||
end
|
||||
end
|
||||
end
|
21
priv/repo/migrations/20230112214231_migrate_resources37.exs
Normal file
21
priv/repo/migrations/20230112214231_migrate_resources37.exs
Normal file
|
@ -0,0 +1,21 @@
|
|||
defmodule AshHq.Repo.Migrations.MigrateResources37 do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
alter table(:users) do
|
||||
modify(:hashed_password, :text, null: true)
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
alter table(:users) do
|
||||
modify(:hashed_password, :text, null: false)
|
||||
end
|
||||
end
|
||||
end
|
25
priv/repo/migrations/20230113044738_migrate_resources38.exs
Normal file
25
priv/repo/migrations/20230113044738_migrate_resources38.exs
Normal file
|
@ -0,0 +1,25 @@
|
|||
defmodule AshHq.Repo.Migrations.MigrateResources38 do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
execute("""
|
||||
DELETE FROM user_tokens
|
||||
""")
|
||||
|
||||
alter table(:user_tokens) do
|
||||
add(:subject, :text, null: false)
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
alter table(:user_tokens) do
|
||||
remove(:subject)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -68,7 +68,7 @@ AshHq.Docs.Library.create!(
|
|||
name: "ash_authentication",
|
||||
display_name: "AshAuthentication",
|
||||
order: 55,
|
||||
repo_org: "alembic",
|
||||
repo_org: "team-alembic",
|
||||
description: """
|
||||
Provides drop-in support for user authentication with various strategies and tons of customizability.
|
||||
"""
|
||||
|
@ -82,7 +82,7 @@ AshHq.Docs.Library.create!(
|
|||
name: "ash_authentication_phoenix",
|
||||
display_name: "AshAuthenticationPhoenix",
|
||||
order: 56,
|
||||
repo_org: "alembic",
|
||||
repo_org: "team-alembic",
|
||||
description: """
|
||||
Phoenix helpers and UI components in support of AshAuthentication.
|
||||
"""
|
||||
|
|
103
priv/resource_snapshots/repo/user_tokens/20230110202444.json
Normal file
103
priv/resource_snapshots/repo/user_tokens/20230110202444.json
Normal file
|
@ -0,0 +1,103 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"now()\")",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "updated_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"now()\")",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "created_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "extra_data",
|
||||
"type": "map"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "purpose",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "expires_at",
|
||||
"type": "utc_datetime"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "jti",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "user_tokens_user_id_fkey",
|
||||
"on_delete": "delete",
|
||||
"on_update": "update",
|
||||
"schema": "public",
|
||||
"table": "users"
|
||||
},
|
||||
"size": null,
|
||||
"source": "user_id",
|
||||
"type": "uuid"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "2ED4D56A2D45C3D5EA222BF1D3D97B80290F1B41F1A0638EA3DD61839798E998",
|
||||
"identities": [],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "user_tokens"
|
||||
}
|
113
priv/resource_snapshots/repo/user_tokens/20230113044738.json
Normal file
113
priv/resource_snapshots/repo/user_tokens/20230113044738.json
Normal file
|
@ -0,0 +1,113 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"now()\")",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "updated_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"now()\")",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "created_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "extra_data",
|
||||
"type": "map"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "purpose",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "expires_at",
|
||||
"type": "utc_datetime"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "subject",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "jti",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "user_tokens_user_id_fkey",
|
||||
"on_delete": "delete",
|
||||
"on_update": "update",
|
||||
"schema": "public",
|
||||
"table": "users"
|
||||
},
|
||||
"size": null,
|
||||
"source": "user_id",
|
||||
"type": "uuid"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "EEDCAEBD942F7D83FCD42C8A85C07B633D3A0A27396E6A40B2E29B490FCE5A49",
|
||||
"identities": [],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "user_tokens"
|
||||
}
|
118
priv/resource_snapshots/repo/users/20230110202444.json
Normal file
118
priv/resource_snapshots/repo/users/20230110202444.json
Normal file
|
@ -0,0 +1,118 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "confirmed_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"uuid_generate_v4()\")",
|
||||
"generated?": false,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "email",
|
||||
"type": "citext"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "hashed_password",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "encrypted_name",
|
||||
"type": "binary"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "encrypted_address",
|
||||
"type": "binary"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "shirt_size",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"now()\")",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "created_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"now()\")",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "updated_at",
|
||||
"type": "utc_datetime_usec"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "EB9257B23461C24189B97F8C36CF47A3FB1F29A798907F16D43AC685C36A2641",
|
||||
"identities": [
|
||||
{
|
||||
"base_filter": null,
|
||||
"index_name": "users_unique_email_index",
|
||||
"keys": [
|
||||
"email"
|
||||
],
|
||||
"name": "unique_email"
|
||||
}
|
||||
],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "users"
|
||||
}
|
118
priv/resource_snapshots/repo/users/20230112214231.json
Normal file
118
priv/resource_snapshots/repo/users/20230112214231.json
Normal file
|
@ -0,0 +1,118 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "confirmed_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"uuid_generate_v4()\")",
|
||||
"generated?": false,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "email",
|
||||
"type": "citext"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "hashed_password",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "encrypted_name",
|
||||
"type": "binary"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "encrypted_address",
|
||||
"type": "binary"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "shirt_size",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"now()\")",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "created_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"now()\")",
|
||||
"generated?": false,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"size": null,
|
||||
"source": "updated_at",
|
||||
"type": "utc_datetime_usec"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "F271D9D0C9CE358D56DD883EDB689726741C92534E44C708AB34AB1F821FF9A2",
|
||||
"identities": [
|
||||
{
|
||||
"base_filter": null,
|
||||
"index_name": "users_unique_email_index",
|
||||
"keys": [
|
||||
"email"
|
||||
],
|
||||
"name": "unique_email"
|
||||
}
|
||||
],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.AshHq.Repo",
|
||||
"schema": null,
|
||||
"table": "users"
|
||||
}
|
|
@ -1,199 +0,0 @@
|
|||
defmodule AshHqWeb.UserAuthTest do
|
||||
use AshHqWeb.ConnCase, async: true
|
||||
|
||||
alias AshHq.Accounts
|
||||
alias AshHqWeb.UserAuth
|
||||
import AshHq.AccountsFixtures
|
||||
|
||||
@remember_me_cookie "_reference_live_app_web_user_remember_me"
|
||||
|
||||
setup %{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> Map.replace!(:secret_key_base, AshHqWeb.Endpoint.config(:secret_key_base))
|
||||
|> init_test_session(%{})
|
||||
|
||||
%{user: user_fixture(), conn: conn}
|
||||
end
|
||||
|
||||
describe "log_in_user/3" do
|
||||
test "stores the user token in the session", %{conn: conn, user: user} do
|
||||
conn = UserAuth.log_in_user(conn, user)
|
||||
assert token = get_session(conn, :user_token)
|
||||
assert get_session(conn, :live_socket_id) == "users_sessions:#{Base.url_encode64(token)}"
|
||||
assert redirected_to(conn) == "/"
|
||||
|
||||
assert AshHq.Accounts.User
|
||||
|> Ash.Query.for_read(:by_token, %{token: token, context: "session"},
|
||||
authorize?: false
|
||||
)
|
||||
|> AshHq.Accounts.read_one!()
|
||||
end
|
||||
|
||||
test "clears everything previously stored in the session", %{conn: conn, user: user} do
|
||||
conn = conn |> put_session(:to_be_removed, "value") |> UserAuth.log_in_user(user)
|
||||
refute get_session(conn, :to_be_removed)
|
||||
end
|
||||
|
||||
test "redirects to the configured path", %{conn: conn, user: user} do
|
||||
conn = conn |> put_session(:user_return_to, "/hello") |> UserAuth.log_in_user(user)
|
||||
assert redirected_to(conn) == "/hello"
|
||||
end
|
||||
|
||||
test "writes a cookie if remember_me is configured", %{conn: conn, user: user} do
|
||||
conn = conn |> fetch_cookies() |> UserAuth.log_in_user(user, %{"remember_me" => "true"})
|
||||
assert get_session(conn, :user_token) == conn.cookies[@remember_me_cookie]
|
||||
|
||||
assert %{value: signed_token, max_age: max_age} = conn.resp_cookies[@remember_me_cookie]
|
||||
assert signed_token != get_session(conn, :user_token)
|
||||
assert max_age == 5_184_000
|
||||
end
|
||||
end
|
||||
|
||||
describe "logout_user/1" do
|
||||
test "erases session and cookies", %{conn: conn, user: user} do
|
||||
user_token =
|
||||
Accounts.UserToken
|
||||
|> Ash.Changeset.for_create(:build_session_token, %{user: user.id}, authorize?: false)
|
||||
|> Accounts.create!()
|
||||
|> Map.get(:token)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_session(:user_token, user_token)
|
||||
|> put_req_cookie(@remember_me_cookie, user_token)
|
||||
|> fetch_cookies()
|
||||
|> UserAuth.log_out_user()
|
||||
|
||||
refute get_session(conn, :user_token)
|
||||
refute conn.cookies[@remember_me_cookie]
|
||||
assert %{max_age: 0} = conn.resp_cookies[@remember_me_cookie]
|
||||
assert redirected_to(conn) == "/"
|
||||
|
||||
refute AshHq.Accounts.User
|
||||
|> Ash.Query.for_read(:by_token, %{token: user_token, context: "session"},
|
||||
authorize?: false
|
||||
)
|
||||
|> AshHq.Accounts.read_one!()
|
||||
end
|
||||
|
||||
test "broadcasts to the given live_socket_id", %{conn: conn} do
|
||||
live_socket_id = "users_sessions:abcdef-token"
|
||||
AshHqWeb.Endpoint.subscribe(live_socket_id)
|
||||
|
||||
conn
|
||||
|> put_session(:live_socket_id, live_socket_id)
|
||||
|> UserAuth.log_out_user()
|
||||
|
||||
assert_receive %Phoenix.Socket.Broadcast{
|
||||
event: "disconnect",
|
||||
topic: "users_sessions:abcdef-token"
|
||||
}
|
||||
end
|
||||
|
||||
test "works even if user is already logged out", %{conn: conn} do
|
||||
conn = conn |> fetch_cookies() |> UserAuth.log_out_user()
|
||||
refute get_session(conn, :user_token)
|
||||
assert %{max_age: 0} = conn.resp_cookies[@remember_me_cookie]
|
||||
assert redirected_to(conn) == "/"
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch_current_user/2" do
|
||||
test "authenticates user from session", %{conn: conn, user: user} do
|
||||
user_token =
|
||||
Accounts.UserToken
|
||||
|> Ash.Changeset.for_create(:build_session_token, %{user: user.id}, authorize?: false)
|
||||
|> Accounts.create!()
|
||||
|> Map.get(:token)
|
||||
|
||||
conn = conn |> put_session(:user_token, user_token) |> UserAuth.fetch_current_user([])
|
||||
assert conn.assigns.current_user.id == user.id
|
||||
end
|
||||
|
||||
test "authenticates user from cookies", %{conn: conn, user: user} do
|
||||
logged_in_conn =
|
||||
conn |> fetch_cookies() |> UserAuth.log_in_user(user, %{"remember_me" => "true"})
|
||||
|
||||
user_token = logged_in_conn.cookies[@remember_me_cookie]
|
||||
%{value: signed_token} = logged_in_conn.resp_cookies[@remember_me_cookie]
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put_req_cookie(@remember_me_cookie, signed_token)
|
||||
|> UserAuth.fetch_current_user([])
|
||||
|
||||
assert get_session(conn, :user_token) == user_token
|
||||
assert conn.assigns.current_user.id == user.id
|
||||
end
|
||||
|
||||
test "does not authenticate if data is missing", %{conn: conn, user: user} do
|
||||
_ =
|
||||
Accounts.UserToken
|
||||
|> Ash.Changeset.for_create(:build_session_token, %{user: user.id}, authorize?: false)
|
||||
|> Accounts.create!()
|
||||
|> Map.get(:token)
|
||||
|
||||
conn = UserAuth.fetch_current_user(conn, [])
|
||||
refute get_session(conn, :user_token)
|
||||
refute conn.assigns.current_user
|
||||
end
|
||||
end
|
||||
|
||||
describe "redirect_if_user_is_authenticated/2" do
|
||||
test "redirects if user is authenticated", %{conn: conn, user: user} do
|
||||
conn = conn |> assign(:current_user, user) |> UserAuth.redirect_if_user_is_authenticated([])
|
||||
assert conn.halted
|
||||
assert redirected_to(conn) == "/"
|
||||
end
|
||||
|
||||
test "does not redirect if user is not authenticated", %{conn: conn} do
|
||||
conn = UserAuth.redirect_if_user_is_authenticated(conn, [])
|
||||
refute conn.halted
|
||||
refute conn.status
|
||||
end
|
||||
end
|
||||
|
||||
describe "require_authenticated_user/2" do
|
||||
test "redirects if user is not authenticated", %{conn: conn} do
|
||||
conn = conn |> fetch_flash() |> UserAuth.require_authenticated_user([])
|
||||
assert conn.halted
|
||||
assert redirected_to(conn) == Routes.app_view_path(conn, :log_in)
|
||||
|
||||
assert Phoenix.Flash.get(conn.assigns[:flash], :error) ==
|
||||
"You must log in to access this page."
|
||||
end
|
||||
|
||||
test "stores the path to redirect to on GET", %{conn: conn} do
|
||||
halted_conn =
|
||||
%{conn | path_info: ["foo"], query_string: ""}
|
||||
|> fetch_flash()
|
||||
|> UserAuth.require_authenticated_user([])
|
||||
|
||||
assert halted_conn.halted
|
||||
assert get_session(halted_conn, :user_return_to) == "/foo"
|
||||
|
||||
halted_conn =
|
||||
%{conn | path_info: ["foo"], query_string: "bar=baz"}
|
||||
|> fetch_flash()
|
||||
|> UserAuth.require_authenticated_user([])
|
||||
|
||||
assert halted_conn.halted
|
||||
assert get_session(halted_conn, :user_return_to) == "/foo?bar=baz"
|
||||
|
||||
halted_conn =
|
||||
%{conn | path_info: ["foo"], query_string: "bar", method: "POST"}
|
||||
|> fetch_flash()
|
||||
|> UserAuth.require_authenticated_user([])
|
||||
|
||||
assert halted_conn.halted
|
||||
refute get_session(halted_conn, :user_return_to)
|
||||
end
|
||||
|
||||
test "does not redirect if user is authenticated", %{conn: conn, user: user} do
|
||||
conn = conn |> assign(:current_user, user) |> UserAuth.require_authenticated_user([])
|
||||
refute conn.halted
|
||||
refute conn.status
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,106 +0,0 @@
|
|||
defmodule AshHqWeb.UserConfirmationControllerTest do
|
||||
use AshHqWeb.ConnCase, async: true
|
||||
|
||||
alias AshHq.Accounts
|
||||
alias AshHq.Repo
|
||||
import AshHq.AccountsFixtures
|
||||
|
||||
setup do
|
||||
user = user_fixture()
|
||||
%{user: user}
|
||||
end
|
||||
|
||||
describe "POST /users/confirm" do
|
||||
@tag :capture_log
|
||||
test "sends a new confirmation token", %{conn: conn, user: user} do
|
||||
conn =
|
||||
post(conn, Routes.user_confirmation_path(conn, :create), %{
|
||||
"user" => %{"email" => user.email}
|
||||
})
|
||||
|
||||
assert redirected_to(conn) == "/"
|
||||
assert Phoenix.Flash.get(conn.assigns[:flash], :info) =~ "If your email is in our system"
|
||||
assert Repo.get_by!(Accounts.UserToken, user_id: user.id).context == "confirm"
|
||||
end
|
||||
|
||||
test "does not send confirmation token if account is confirmed", %{conn: conn, user: user} do
|
||||
Repo.delete_all(Accounts.UserToken)
|
||||
|
||||
user
|
||||
|> Ash.Changeset.for_update(:confirm, %{}, authorize?: false)
|
||||
|> Accounts.update!()
|
||||
|
||||
conn =
|
||||
post(conn, Routes.user_confirmation_path(conn, :create), %{
|
||||
"user" => %{"email" => user.email}
|
||||
})
|
||||
|
||||
assert redirected_to(conn) == "/"
|
||||
assert Phoenix.Flash.get(conn.assigns[:flash], :info) =~ "If your email is in our system"
|
||||
refute Repo.get_by(Accounts.UserToken, user_id: user.id)
|
||||
end
|
||||
|
||||
test "does not send confirmation token if email is invalid", %{conn: conn} do
|
||||
Repo.delete_all(Accounts.UserToken)
|
||||
|
||||
conn =
|
||||
post(conn, Routes.user_confirmation_path(conn, :create), %{
|
||||
"user" => %{"email" => "unknown@example.com"}
|
||||
})
|
||||
|
||||
assert redirected_to(conn) == "/"
|
||||
assert Phoenix.Flash.get(conn.assigns[:flash], :info) =~ "If your email is in our system"
|
||||
assert Repo.all(Accounts.UserToken) == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /users/confirm/:token" do
|
||||
test "confirms the given token once", %{conn: conn, user: user} do
|
||||
token =
|
||||
user
|
||||
|> Ash.Changeset.for_update(:deliver_user_confirmation_instructions, %{},
|
||||
authorize?: false
|
||||
)
|
||||
|> Accounts.update!()
|
||||
|> Map.get(:__metadata__)
|
||||
|> Map.get(:token)
|
||||
|
||||
conn = get(conn, Routes.user_confirmation_path(conn, :confirm, token))
|
||||
assert redirected_to(conn) == "/"
|
||||
|
||||
assert Phoenix.Flash.get(conn.assigns[:flash], :info) &&
|
||||
Phoenix.Flash.get(conn.assigns[:flash], :info) =~ "Account confirmed successfully"
|
||||
|
||||
assert Accounts.get!(Accounts.User, user.id, authorize?: false).confirmed_at
|
||||
|
||||
refute get_session(conn, :user_token)
|
||||
assert Repo.all(Accounts.UserToken) == []
|
||||
|
||||
# When not logged in
|
||||
conn = get(conn, Routes.user_confirmation_path(conn, :confirm, token))
|
||||
assert redirected_to(conn) == "/"
|
||||
|
||||
assert Phoenix.Flash.get(conn.assigns[:flash], :error) =~
|
||||
"Account confirmation link is invalid or it has expired"
|
||||
|
||||
# When logged in
|
||||
conn =
|
||||
build_conn()
|
||||
|> log_in_user(user)
|
||||
|> get(Routes.user_confirmation_path(conn, :confirm, token))
|
||||
|
||||
assert redirected_to(conn) == "/"
|
||||
refute Phoenix.Flash.get(conn.assigns[:flash], :error)
|
||||
end
|
||||
|
||||
test "does not confirm email with invalid token", %{conn: conn, user: user} do
|
||||
conn = get(conn, Routes.user_confirmation_path(conn, :confirm, "oops"))
|
||||
assert redirected_to(conn) == "/"
|
||||
|
||||
assert Phoenix.Flash.get(conn.assigns[:flash], :error) =~
|
||||
"Account confirmation link is invalid or it has expired"
|
||||
|
||||
refute Accounts.get!(Accounts.User, user.id, authorize?: false).confirmed_at
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,60 +0,0 @@
|
|||
defmodule AshHqWeb.UserSessionControllerTest do
|
||||
use AshHqWeb.ConnCase, async: true
|
||||
|
||||
import AshHq.AccountsFixtures
|
||||
|
||||
setup do
|
||||
%{user: user_fixture()}
|
||||
end
|
||||
|
||||
describe "POST /users/new_session/:token" do
|
||||
test "logs the user in", %{conn: conn, user: user} do
|
||||
token = AshHqWeb.UserAuth.create_token_for_user(user)
|
||||
|
||||
conn =
|
||||
post(conn, Routes.user_session_path(conn, :log_in), %{
|
||||
"log_in" => %{"token" => Base.url_encode64(token, padding: false)}
|
||||
})
|
||||
|
||||
assert get_session(conn, :user_token)
|
||||
assert redirected_to(conn) =~ "/"
|
||||
|
||||
# Now do a logged in request and assert on the menu
|
||||
conn = get(conn, "/")
|
||||
response = html_response(conn, 200)
|
||||
|
||||
assert response =~ "Ash Framework"
|
||||
end
|
||||
|
||||
test "logs the user in with remember me", %{conn: conn, user: user} do
|
||||
token = AshHqWeb.UserAuth.create_token_for_user(user)
|
||||
|
||||
conn =
|
||||
post(conn, Routes.user_session_path(conn, :log_in), %{
|
||||
"log_in" => %{
|
||||
"token" => Base.url_encode64(token, padding: false),
|
||||
"remember_me" => "true"
|
||||
}
|
||||
})
|
||||
|
||||
assert conn.resp_cookies["_reference_live_app_web_user_remember_me"]
|
||||
assert redirected_to(conn) =~ "/"
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /users/log_out" do
|
||||
test "logs the user out", %{conn: conn, user: user} do
|
||||
conn = conn |> log_in_user(user) |> post(Routes.user_session_path(conn, :delete))
|
||||
assert redirected_to(conn) == "/"
|
||||
refute get_session(conn, :user_token)
|
||||
assert Phoenix.Flash.get(conn.assigns[:flash], :info) =~ "Logged out successfully"
|
||||
end
|
||||
|
||||
test "succeeds even if the user is not logged in", %{conn: conn} do
|
||||
conn = post(conn, Routes.user_session_path(conn, :delete))
|
||||
assert redirected_to(conn) == "/"
|
||||
refute get_session(conn, :user_token)
|
||||
assert Phoenix.Flash.get(conn.assigns[:flash], :info) =~ "Logged out successfully"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,63 +0,0 @@
|
|||
defmodule AshHqWeb.UserSettingsControllerTest do
|
||||
use AshHqWeb.ConnCase, async: true
|
||||
|
||||
alias AshHq.Accounts
|
||||
import AshHq.AccountsFixtures
|
||||
|
||||
setup :register_and_log_in_user
|
||||
|
||||
describe "GET /users/settings/confirm_email/:token" do
|
||||
setup %{user: user} do
|
||||
email = unique_user_email()
|
||||
|
||||
token =
|
||||
user
|
||||
|> Ash.Changeset.for_update(
|
||||
:deliver_update_email_instructions,
|
||||
%{
|
||||
email: email,
|
||||
current_password: valid_user_password(),
|
||||
update_url_fun: &Routes.user_settings_url(AshHqWeb.Endpoint, :confirm_email, &1)
|
||||
},
|
||||
authorize?: false
|
||||
)
|
||||
|> Accounts.update!()
|
||||
|> Map.get(:__metadata__)
|
||||
|> Map.get(:token)
|
||||
|
||||
%{token: token, email: email}
|
||||
end
|
||||
|
||||
test "updates the user email once", %{conn: conn, user: user, token: token, email: email} do
|
||||
conn = get(conn, Routes.user_settings_path(conn, :confirm_email, token))
|
||||
assert redirected_to(conn) == Routes.app_view_path(conn, :user_settings)
|
||||
assert Phoenix.Flash.get(conn.assigns[:flash], :info) =~ "Email changed successfully"
|
||||
|
||||
refute Accounts.get!(Accounts.User, [email: user.email], authorize?: false, error?: false)
|
||||
|
||||
assert Accounts.get!(Accounts.User, [email: email], authorize?: false)
|
||||
|
||||
conn = get(conn, Routes.user_settings_path(conn, :confirm_email, token))
|
||||
assert redirected_to(conn) == Routes.app_view_path(conn, :user_settings)
|
||||
|
||||
assert Phoenix.Flash.get(conn.assigns[:flash], :error) =~
|
||||
"Email change link is invalid or it has expired"
|
||||
end
|
||||
|
||||
test "does not update email with invalid token", %{conn: conn, user: user} do
|
||||
conn = get(conn, Routes.user_settings_path(conn, :confirm_email, "oops"))
|
||||
assert redirected_to(conn) == Routes.app_view_path(conn, :user_settings)
|
||||
|
||||
assert Phoenix.Flash.get(conn.assigns[:flash], :error) =~
|
||||
"Email change link is invalid or it has expired"
|
||||
|
||||
assert Accounts.get!(Accounts.User, [email: user.email], authorize?: false)
|
||||
end
|
||||
|
||||
test "redirects if user is not logged in", %{token: token} do
|
||||
conn = build_conn()
|
||||
conn = get(conn, Routes.user_settings_path(conn, :confirm_email, token))
|
||||
assert redirected_to(conn) == Routes.app_view_path(conn, :log_in)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,46 +0,0 @@
|
|||
defmodule AshHq.LogInTest do
|
||||
use AshHqWeb.ConnCase
|
||||
|
||||
import Plug.Conn
|
||||
import Phoenix.ConnTest
|
||||
import Phoenix.LiveViewTest
|
||||
|
||||
@endpoint AshHqWeb.Endpoint
|
||||
|
||||
setup :register_user
|
||||
|
||||
describe "log in form" do
|
||||
test "renders", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, "/users/log_in")
|
||||
|
||||
assert html =~ "Log In"
|
||||
end
|
||||
|
||||
test "submission logs you in", %{conn: conn, user: user} do
|
||||
{:ok, view, _html} = live(conn, "/users/log_in")
|
||||
|
||||
form = form(view, "form#log_in", log_in: %{email: user.email, password: "hello world!"})
|
||||
|
||||
assert form
|
||||
|> render_submit() =~ "phx-trigger-action"
|
||||
|
||||
conn = follow_trigger_action(form, conn)
|
||||
|
||||
conn = fetch_session(conn)
|
||||
assert get_session(conn, :user_token)
|
||||
end
|
||||
|
||||
test "submission with a bad password does not log you in", %{conn: conn, user: user} do
|
||||
{:ok, view, _html} = live(conn, "/users/log_in")
|
||||
|
||||
form = form(view, "form#log_in", log_in: %{email: user.email, password: "bad password!"})
|
||||
|
||||
assert {:ok, _view, html} =
|
||||
form
|
||||
|> render_submit()
|
||||
|> follow_redirect(conn)
|
||||
|
||||
assert html =~ "Invalid username or password"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,53 +0,0 @@
|
|||
# defmodule AshHqWeb.UserRegistrationControllerTest do
|
||||
# use AshHqWeb.ConnCase, async: true
|
||||
|
||||
# import AshHq.AccountsFixtures
|
||||
|
||||
# describe "GET /users/register" do
|
||||
# test "renders registration page", %{conn: conn} do
|
||||
# conn = get(conn, Routes.user_registration_path(conn, :new))
|
||||
# response = html_response(conn, 200)
|
||||
# assert response =~ "Register</h5>"
|
||||
# assert response =~ "Log in</a>"
|
||||
# assert response =~ "Log in</a>"
|
||||
# end
|
||||
|
||||
# test "redirects if already logged in", %{conn: conn} do
|
||||
# conn = conn |> log_in_user(user_fixture()) |> get(Routes.user_registration_path(conn, :new))
|
||||
# assert redirected_to(conn) == "/"
|
||||
# end
|
||||
# end
|
||||
|
||||
# describe "POST /users/register" do
|
||||
# @tag :capture_log
|
||||
# test "creates account and logs the user in", %{conn: conn} do
|
||||
# email = unique_user_email()
|
||||
|
||||
# conn =
|
||||
# post(conn, Routes.user_registration_path(conn, :create), %{
|
||||
# "user" => %{"email" => email, "password" => valid_user_password()}
|
||||
# })
|
||||
|
||||
# assert get_session(conn, :user_token)
|
||||
# assert redirected_to(conn) =~ "/"
|
||||
|
||||
# # Now do a logged in request and assert on the menu
|
||||
# conn = get(conn, "/")
|
||||
# response = html_response(conn, 200)
|
||||
|
||||
# assert response =~ "Ash Framework"
|
||||
# end
|
||||
|
||||
# test "render errors for invalid data", %{conn: conn} do
|
||||
# conn =
|
||||
# post(conn, Routes.user_registration_path(conn, :create), %{
|
||||
# "user" => %{"email" => "with spaces", "password" => "too short"}
|
||||
# })
|
||||
|
||||
# response = html_response(conn, 200)
|
||||
# assert response =~ "Register</h5>"
|
||||
# assert response =~ "must have the @ sign and no spaces"
|
||||
# assert response =~ "length must be greater than or equal to 12"
|
||||
# end
|
||||
# end
|
||||
# end
|
|
@ -1,124 +0,0 @@
|
|||
# defmodule AshHqWeb.UserResetPasswordControllerTest do
|
||||
# use AshHqWeb.ConnCase, async: true
|
||||
|
||||
# alias AshHq.Accounts
|
||||
# alias AshHq.Repo
|
||||
# import AshHq.AccountsFixtures
|
||||
|
||||
# setup do
|
||||
# %{user: user_fixture()}
|
||||
# end
|
||||
|
||||
# describe "GET /users/reset_password" do
|
||||
# test "renders the reset password page", %{conn: conn} do
|
||||
# conn = get(conn, Routes.user_reset_password_path(conn, :new))
|
||||
# response = html_response(conn, 200)
|
||||
# assert response =~ "Forgot your password?</h5>"
|
||||
# end
|
||||
# end
|
||||
|
||||
# describe "POST /users/reset_password" do
|
||||
# @tag :capture_log
|
||||
# test "sends a new reset password token", %{conn: conn, user: user} do
|
||||
# conn =
|
||||
# post(conn, Routes.user_reset_password_path(conn, :create), %{
|
||||
# "user" => %{"email" => user.email}
|
||||
# })
|
||||
|
||||
# assert redirected_to(conn) == "/"
|
||||
# assert Phoenix.Flash.get(conn.assigns[:flash], :info) =~ "If your email is in our system"
|
||||
# assert Repo.get_by!(Accounts.UserToken, user_id: user.id).context == "reset_password"
|
||||
# end
|
||||
|
||||
# test "does not send reset password token if email is invalid", %{conn: conn} do
|
||||
# conn =
|
||||
# post(conn, Routes.user_reset_password_path(conn, :create), %{
|
||||
# "user" => %{"email" => "unknown@example.com"}
|
||||
# })
|
||||
|
||||
# assert redirected_to(conn) == "/"
|
||||
# assert Phoenix.Flash.get(conn.assigns[:flash], :info) =~ "If your email is in our system"
|
||||
# assert Repo.all(Accounts.UserToken) == []
|
||||
# end
|
||||
# end
|
||||
|
||||
# describe "GET /users/reset_password/:token" do
|
||||
# setup %{user: user} do
|
||||
# token =
|
||||
# user
|
||||
# |> Ash.Changeset.for_update(:deliver_user_reset_password_instructions, %{}, authorize?: false)
|
||||
# |> Accounts.update!()
|
||||
# |> Map.get(:__metadata__)
|
||||
# |> Map.get(:token)
|
||||
|
||||
# %{token: token}
|
||||
# end
|
||||
|
||||
# test "renders reset password", %{conn: conn, token: token} do
|
||||
# conn = get(conn, Routes.user_reset_password_path(conn, :edit, token))
|
||||
# assert html_response(conn, 200) =~ "Reset Password</h5>"
|
||||
# end
|
||||
|
||||
# test "does not render reset password with invalid token", %{conn: conn} do
|
||||
# conn = get(conn, Routes.user_reset_password_path(conn, :edit, "oops"))
|
||||
# assert redirected_to(conn) == "/"
|
||||
# assert Phoenix.Flash.get(conn.assigns[:flash], :error) =~ "Reset password link is invalid or it has expired"
|
||||
# end
|
||||
# end
|
||||
|
||||
# describe "PUT /users/reset_password/:token" do
|
||||
# setup %{user: user} do
|
||||
# token =
|
||||
# user
|
||||
# |> Ash.Changeset.for_update(:deliver_user_reset_password_instructions, %{}, authorize?: false)
|
||||
# |> Accounts.update!()
|
||||
# |> Map.get(:__metadata__)
|
||||
# |> Map.get(:token)
|
||||
|
||||
# %{token: token}
|
||||
# end
|
||||
|
||||
# test "resets password once", %{conn: conn, user: user, token: token} do
|
||||
# conn =
|
||||
# put(conn, Routes.user_reset_password_path(conn, :update, token), %{
|
||||
# "user" => %{
|
||||
# "current_password" => "hello world!",
|
||||
# "password" => "new valid password",
|
||||
# "password_confirmation" => "new valid password"
|
||||
# }
|
||||
# })
|
||||
|
||||
# assert redirected_to(conn) == Routes.app_view_path(conn, :log_in)
|
||||
# refute get_session(conn, :user_token)
|
||||
# assert Phoenix.Flash.get(conn.assigns[:flash], :info) =~ "Password reset successfully"
|
||||
|
||||
# assert Accounts.User
|
||||
# |> Ash.Query.for_read(:by_email_and_password, %{
|
||||
# email: user.email,
|
||||
# password: "new valid password"
|
||||
# }, authorize?: false)
|
||||
# |> Accounts.read_one!()
|
||||
# end
|
||||
|
||||
# test "does not reset password on invalid data", %{conn: conn, token: token} do
|
||||
# conn =
|
||||
# put(conn, Routes.user_reset_password_path(conn, :update, token), %{
|
||||
# "user" => %{
|
||||
# "password" => "too short",
|
||||
# "password_confirmation" => "does not match"
|
||||
# }
|
||||
# })
|
||||
|
||||
# response = html_response(conn, 200)
|
||||
# assert response =~ "Reset Password</h5>"
|
||||
# assert response =~ "length must be greater than or equal to 12"
|
||||
# assert response =~ "Confirmation did not match value"
|
||||
# end
|
||||
|
||||
# test "does not reset password with invalid token", %{conn: conn} do
|
||||
# conn = put(conn, Routes.user_reset_password_path(conn, :update, "oops"))
|
||||
# assert redirected_to(conn) == "/"
|
||||
# assert Phoenix.Flash.get(conn.assigns[:flash], :error) =~ "Reset password link is invalid or it has expired"
|
||||
# end
|
||||
# end
|
||||
# end
|
|
@ -22,7 +22,7 @@ defmodule AshHq.SettingsTest do
|
|||
view
|
||||
|> element("form#update_email")
|
||||
|> render_submit(%{
|
||||
update_email: %{email: "new_email@example.com", current_password: "hello world!"}
|
||||
update_email: %{email: "new_email@example.com", current_password: "password123"}
|
||||
})
|
||||
|> follow_redirect(conn)
|
||||
|
||||
|
@ -35,7 +35,7 @@ defmodule AshHq.SettingsTest do
|
|||
view
|
||||
|> element("form#update_email")
|
||||
|> render_submit(%{
|
||||
update_email: %{email: "new_email@example.com", current_password: "hello world!"}
|
||||
update_email: %{email: "new_email@example.com", current_password: "password123"}
|
||||
})
|
||||
|
||||
assert_received {:email, email}
|
||||
|
@ -51,16 +51,18 @@ defmodule AshHq.SettingsTest do
|
|||
view
|
||||
|> element("form#update_email")
|
||||
|> render_submit(%{
|
||||
update_email: %{email: "new_email@example.com", current_password: "hello world!"}
|
||||
update_email: %{email: "new_email@example.com", current_password: "password123"}
|
||||
})
|
||||
|
||||
assert_received {:email, email}
|
||||
|
||||
assert %{"url" => url} = Regex.named_captures(~r/(?<url>http[^\s\"]*)/, email.html_body)
|
||||
|
||||
path = URI.parse(url).path
|
||||
uri = URI.parse(url)
|
||||
|
||||
assert {:ok, _conn} = conn |> live(path) |> follow_redirect(conn, "/users/settings")
|
||||
path = %{uri | authority: nil, host: nil, scheme: nil, port: nil} |> to_string()
|
||||
|
||||
assert {:ok, _conn} = conn |> live(path) |> follow_redirect(conn, "/")
|
||||
|
||||
new_user = AshHq.Accounts.reload!(user, authorize?: false)
|
||||
assert to_string(new_user.email) == "new_email@example.com"
|
||||
|
@ -84,7 +86,7 @@ defmodule AshHq.SettingsTest do
|
|||
change_password: %{
|
||||
password: "hello world2!",
|
||||
password_confirmation: "hello world2!",
|
||||
current_password: "hello world!"
|
||||
current_password: "password123"
|
||||
}
|
||||
})
|
||||
|> follow_redirect(conn)
|
||||
|
@ -102,7 +104,7 @@ defmodule AshHq.SettingsTest do
|
|||
change_password: %{
|
||||
password: "hello world2!",
|
||||
password_confirmation: "hello world2!",
|
||||
current_password: "hello world!"
|
||||
current_password: "password123"
|
||||
}
|
||||
})
|
||||
|> follow_redirect(conn)
|
||||
|
@ -110,7 +112,7 @@ defmodule AshHq.SettingsTest do
|
|||
assert html =~ "Password has been successfully changed"
|
||||
|
||||
assert AshHq.Accounts.User
|
||||
|> Ash.Query.for_read(:by_email_and_password, %{
|
||||
|> Ash.Query.for_read(:sign_in_with_password, %{
|
||||
email: user.email,
|
||||
password: "hello world2!"
|
||||
})
|
||||
|
|
|
@ -46,7 +46,8 @@ defmodule AshHqWeb.ConnCase do
|
|||
test context.
|
||||
"""
|
||||
def register_and_log_in_user(%{conn: conn}) do
|
||||
user = AshHq.AccountsFixtures.user_fixture()
|
||||
user = test_user()
|
||||
|
||||
%{conn: log_in_user(conn, user), user: user}
|
||||
end
|
||||
|
||||
|
@ -55,8 +56,30 @@ defmodule AshHqWeb.ConnCase do
|
|||
|
||||
setup :register_user
|
||||
"""
|
||||
def register_user(context) do
|
||||
%{user: AshHq.AccountsFixtures.user_fixture()}
|
||||
def register_user(_context) do
|
||||
%{
|
||||
user: test_user()
|
||||
}
|
||||
end
|
||||
|
||||
defp test_user do
|
||||
user =
|
||||
AshHq.Accounts.User.register_with_password!(
|
||||
"test@example.com",
|
||||
"password123",
|
||||
"password123",
|
||||
authorize?: false
|
||||
)
|
||||
|
||||
receive do
|
||||
{:email, _} ->
|
||||
:ok
|
||||
after
|
||||
0 ->
|
||||
:ok
|
||||
end
|
||||
|
||||
user
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -65,15 +88,8 @@ defmodule AshHqWeb.ConnCase do
|
|||
It returns an updated `conn`.
|
||||
"""
|
||||
def log_in_user(conn, user) do
|
||||
token =
|
||||
AshHq.Accounts.UserToken
|
||||
|> Ash.Changeset.for_create(:build_session_token, %{user: user.id}, authorize?: false)
|
||||
|> AshHq.Accounts.create!()
|
||||
|> Map.get(:__metadata__)
|
||||
|> Map.get(:url_token)
|
||||
|
||||
conn
|
||||
|> Phoenix.ConnTest.init_test_session(%{})
|
||||
|> Plug.Conn.put_session(:user_token, token)
|
||||
|> Plug.Conn.put_session(:user_token, user.__metadata__.token)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
defmodule AshHq.AccountsFixtures do
|
||||
@moduledoc """
|
||||
This module defines test helpers for creating
|
||||
entities via the `AshHq.Accounts` context.
|
||||
"""
|
||||
|
||||
def unique_user_email, do: "user#{System.unique_integer()}@example.com"
|
||||
def valid_user_password, do: "hello world!"
|
||||
|
||||
def user_fixture(attrs \\ %{}) do
|
||||
params =
|
||||
Enum.into(attrs, %{
|
||||
email: unique_user_email(),
|
||||
password: valid_user_password(),
|
||||
confirmation_url_fun:
|
||||
&AshHqWeb.Router.Helpers.user_confirmation_url(AshHqWeb.Endpoint, :confirm, &1)
|
||||
})
|
||||
|
||||
user =
|
||||
AshHq.Accounts.User
|
||||
|> Ash.Changeset.for_create(:register, params, authorize?: false)
|
||||
|> AshHq.Accounts.create!()
|
||||
|
||||
Swoosh.TestAssertions.assert_email_sent()
|
||||
|
||||
user
|
||||
end
|
||||
end
|
|
@ -1 +1,2 @@
|
|||
:erlang.system_flag(:backtrace_depth, 100)
|
||||
ExUnit.start()
|
||||
|
|
Loading…
Reference in a new issue