🔥 initial commit 🔥
This commit is contained in:
13
lib/mixer/accounts.ex
Normal file
13
lib/mixer/accounts.ex
Normal file
@@ -0,0 +1,13 @@
|
||||
defmodule Mixer.Accounts do
|
||||
use Ash.Domain, otp_app: :mixer, extensions: [AshAdmin.Domain]
|
||||
|
||||
admin do
|
||||
show? true
|
||||
end
|
||||
|
||||
resources do
|
||||
resource Mixer.Accounts.Token
|
||||
resource Mixer.Accounts.User
|
||||
resource Mixer.Accounts.ApiKey
|
||||
end
|
||||
end
|
||||
55
lib/mixer/accounts/api_key.ex
Normal file
55
lib/mixer/accounts/api_key.ex
Normal file
@@ -0,0 +1,55 @@
|
||||
defmodule Mixer.Accounts.ApiKey do
|
||||
use Ash.Resource,
|
||||
otp_app: :mixer,
|
||||
domain: Mixer.Accounts,
|
||||
data_layer: AshPostgres.DataLayer,
|
||||
authorizers: [Ash.Policy.Authorizer]
|
||||
|
||||
postgres do
|
||||
table "api_keys"
|
||||
repo Mixer.Repo
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:read, :destroy]
|
||||
|
||||
create :create do
|
||||
primary? true
|
||||
accept [:user_id, :expires_at]
|
||||
|
||||
change {AshAuthentication.Strategy.ApiKey.GenerateApiKey,
|
||||
prefix: :mixer, hash: :api_key_hash}
|
||||
end
|
||||
end
|
||||
|
||||
policies do
|
||||
bypass AshAuthentication.Checks.AshAuthenticationInteraction do
|
||||
authorize_if always()
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
attribute :api_key_hash, :binary do
|
||||
allow_nil? false
|
||||
sensitive? true
|
||||
end
|
||||
|
||||
attribute :expires_at, :utc_datetime_usec do
|
||||
allow_nil? false
|
||||
end
|
||||
end
|
||||
|
||||
relationships do
|
||||
belongs_to :user, Mixer.Accounts.User
|
||||
end
|
||||
|
||||
calculations do
|
||||
calculate :valid, :boolean, expr(expires_at > now())
|
||||
end
|
||||
|
||||
identities do
|
||||
identity :unique_api_key, [:api_key_hash]
|
||||
end
|
||||
end
|
||||
114
lib/mixer/accounts/token.ex
Normal file
114
lib/mixer/accounts/token.ex
Normal file
@@ -0,0 +1,114 @@
|
||||
defmodule Mixer.Accounts.Token do
|
||||
use Ash.Resource,
|
||||
otp_app: :mixer,
|
||||
domain: Mixer.Accounts,
|
||||
data_layer: AshPostgres.DataLayer,
|
||||
authorizers: [Ash.Policy.Authorizer],
|
||||
extensions: [AshAuthentication.TokenResource]
|
||||
|
||||
postgres do
|
||||
table "tokens"
|
||||
repo Mixer.Repo
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:read]
|
||||
|
||||
read :expired do
|
||||
description "Look up all expired tokens."
|
||||
filter expr(expires_at < now())
|
||||
end
|
||||
|
||||
read :get_token do
|
||||
description "Look up a token by JTI or token, and an optional purpose."
|
||||
get? true
|
||||
argument :token, :string, sensitive?: true
|
||||
argument :jti, :string, sensitive?: true
|
||||
argument :purpose, :string, sensitive?: false
|
||||
|
||||
prepare AshAuthentication.TokenResource.GetTokenPreparation
|
||||
end
|
||||
|
||||
action :revoked?, :boolean do
|
||||
description "Returns true if a revocation token is found for the provided token"
|
||||
argument :token, :string, sensitive?: true
|
||||
argument :jti, :string, sensitive?: true
|
||||
|
||||
run AshAuthentication.TokenResource.IsRevoked
|
||||
end
|
||||
|
||||
create :revoke_token do
|
||||
description "Revoke a token. Creates a revocation token corresponding to the provided token."
|
||||
accept [:extra_data]
|
||||
argument :token, :string, allow_nil?: false, sensitive?: true
|
||||
|
||||
change AshAuthentication.TokenResource.RevokeTokenChange
|
||||
end
|
||||
|
||||
create :revoke_jti do
|
||||
description "Revoke a token by JTI. Creates a revocation token corresponding to the provided jti."
|
||||
accept [:extra_data]
|
||||
argument :subject, :string, allow_nil?: false, sensitive?: true
|
||||
argument :jti, :string, allow_nil?: false, sensitive?: true
|
||||
|
||||
change AshAuthentication.TokenResource.RevokeJtiChange
|
||||
end
|
||||
|
||||
create :store_token do
|
||||
description "Stores a token used for the provided purpose."
|
||||
accept [:extra_data, :purpose]
|
||||
argument :token, :string, allow_nil?: false, sensitive?: true
|
||||
change AshAuthentication.TokenResource.StoreTokenChange
|
||||
end
|
||||
|
||||
destroy :expunge_expired do
|
||||
description "Deletes expired tokens."
|
||||
change filter(expr(expires_at < now()))
|
||||
end
|
||||
|
||||
update :revoke_all_stored_for_subject do
|
||||
description "Revokes all stored tokens for a specific subject."
|
||||
accept [:extra_data]
|
||||
argument :subject, :string, allow_nil?: false, sensitive?: true
|
||||
change AshAuthentication.TokenResource.RevokeAllStoredForSubjectChange
|
||||
end
|
||||
end
|
||||
|
||||
policies do
|
||||
bypass AshAuthentication.Checks.AshAuthenticationInteraction do
|
||||
description "AshAuthentication can interact with the token resource"
|
||||
authorize_if always()
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
attribute :jti, :string do
|
||||
primary_key? true
|
||||
public? true
|
||||
allow_nil? false
|
||||
sensitive? true
|
||||
end
|
||||
|
||||
attribute :subject, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :expires_at, :utc_datetime do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :purpose, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :extra_data, :map do
|
||||
public? true
|
||||
end
|
||||
|
||||
create_timestamp :created_at
|
||||
update_timestamp :updated_at
|
||||
end
|
||||
end
|
||||
311
lib/mixer/accounts/user.ex
Normal file
311
lib/mixer/accounts/user.ex
Normal file
@@ -0,0 +1,311 @@
|
||||
defmodule Mixer.Accounts.User do
|
||||
use Ash.Resource,
|
||||
otp_app: :mixer,
|
||||
domain: Mixer.Accounts,
|
||||
data_layer: AshPostgres.DataLayer,
|
||||
authorizers: [Ash.Policy.Authorizer],
|
||||
extensions: [AshAuthentication]
|
||||
|
||||
authentication do
|
||||
add_ons do
|
||||
log_out_everywhere do
|
||||
apply_on_password_change? true
|
||||
end
|
||||
|
||||
confirmation :confirm_new_user do
|
||||
monitor_fields [:email]
|
||||
confirm_on_create? true
|
||||
confirm_on_update? false
|
||||
require_interaction? true
|
||||
confirmed_at_field :confirmed_at
|
||||
auto_confirm_actions [:sign_in_with_magic_link, :reset_password_with_token]
|
||||
sender Mixer.Accounts.User.Senders.SendNewUserConfirmationEmail
|
||||
end
|
||||
end
|
||||
|
||||
tokens do
|
||||
enabled? true
|
||||
token_resource Mixer.Accounts.Token
|
||||
signing_secret Mixer.Secrets
|
||||
store_all_tokens? true
|
||||
require_token_presence_for_authentication? true
|
||||
end
|
||||
|
||||
strategies do
|
||||
password :password do
|
||||
identity_field :email
|
||||
hash_provider AshAuthentication.BcryptProvider
|
||||
|
||||
resettable do
|
||||
sender Mixer.Accounts.User.Senders.SendPasswordResetEmail
|
||||
# these configurations will be the default in a future release
|
||||
password_reset_action_name :reset_password_with_token
|
||||
request_password_reset_action_name :request_password_reset_token
|
||||
end
|
||||
end
|
||||
|
||||
remember_me :remember_me
|
||||
|
||||
magic_link do
|
||||
identity_field :email
|
||||
registration_enabled? true
|
||||
require_interaction? true
|
||||
|
||||
sender Mixer.Accounts.User.Senders.SendMagicLinkEmail
|
||||
end
|
||||
|
||||
api_key :api_key do
|
||||
api_key_relationship :valid_api_keys
|
||||
api_key_hash_attribute :api_key_hash
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
postgres do
|
||||
table "users"
|
||||
repo Mixer.Repo
|
||||
end
|
||||
|
||||
actions do
|
||||
defaults [:read]
|
||||
|
||||
read :get_by_subject do
|
||||
description "Get a user by the subject claim in a JWT"
|
||||
argument :subject, :string, allow_nil?: false
|
||||
get? true
|
||||
prepare AshAuthentication.Preparations.FilterBySubject
|
||||
end
|
||||
|
||||
update :change_password do
|
||||
# Use this action to allow users to change their password by providing
|
||||
# their current password and a new password.
|
||||
|
||||
require_atomic? false
|
||||
accept []
|
||||
argument :current_password, :string, sensitive?: true, allow_nil?: false
|
||||
|
||||
argument :password, :string,
|
||||
sensitive?: true,
|
||||
allow_nil?: false,
|
||||
constraints: [min_length: 8]
|
||||
|
||||
argument :password_confirmation, :string, sensitive?: true, allow_nil?: false
|
||||
|
||||
validate confirm(:password, :password_confirmation)
|
||||
|
||||
validate {AshAuthentication.Strategy.Password.PasswordValidation,
|
||||
strategy_name: :password, password_argument: :current_password}
|
||||
|
||||
change {AshAuthentication.Strategy.Password.HashPasswordChange, strategy_name: :password}
|
||||
end
|
||||
|
||||
read :sign_in_with_password do
|
||||
description "Attempt to sign in using a email and password."
|
||||
get? true
|
||||
|
||||
argument :email, :ci_string do
|
||||
description "The email to use for retrieving the user."
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
argument :password, :string do
|
||||
description "The password to check for the matching user."
|
||||
allow_nil? false
|
||||
sensitive? true
|
||||
end
|
||||
|
||||
# validates the provided email and password and generates a token
|
||||
prepare AshAuthentication.Strategy.Password.SignInPreparation
|
||||
|
||||
metadata :token, :string do
|
||||
description "A JWT that can be used to authenticate the user."
|
||||
allow_nil? false
|
||||
end
|
||||
end
|
||||
|
||||
read :sign_in_with_token do
|
||||
# In the generated sign in components, we validate the
|
||||
# email and password directly in the LiveView
|
||||
# and generate a short-lived token that can be used to sign in over
|
||||
# a standard controller action, exchanging it for a standard token.
|
||||
# This action performs that exchange. If you do not use the generated
|
||||
# liveviews, you may remove this action, and set
|
||||
# `sign_in_tokens_enabled? false` in the password strategy.
|
||||
|
||||
description "Attempt to sign in using a short-lived sign in token."
|
||||
get? true
|
||||
|
||||
argument :token, :string do
|
||||
description "The short-lived sign in token."
|
||||
allow_nil? false
|
||||
sensitive? true
|
||||
end
|
||||
|
||||
# validates the provided sign in token and generates a token
|
||||
prepare AshAuthentication.Strategy.Password.SignInWithTokenPreparation
|
||||
|
||||
metadata :token, :string do
|
||||
description "A JWT that can be used to authenticate the user."
|
||||
allow_nil? false
|
||||
end
|
||||
end
|
||||
|
||||
create :register_with_password do
|
||||
description "Register a new user with a email and password."
|
||||
|
||||
argument :email, :ci_string do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
argument :password, :string do
|
||||
description "The proposed password for the user, in plain text."
|
||||
allow_nil? false
|
||||
constraints min_length: 8
|
||||
sensitive? true
|
||||
end
|
||||
|
||||
argument :password_confirmation, :string do
|
||||
description "The proposed password for the user (again), in plain text."
|
||||
allow_nil? false
|
||||
sensitive? true
|
||||
end
|
||||
|
||||
# Sets the email from the argument
|
||||
change set_attribute(:email, arg(:email))
|
||||
|
||||
# Hashes the provided password
|
||||
change AshAuthentication.Strategy.Password.HashPasswordChange
|
||||
|
||||
# Generates an authentication token for the user
|
||||
change AshAuthentication.GenerateTokenChange
|
||||
|
||||
# validates that the password matches the confirmation
|
||||
validate AshAuthentication.Strategy.Password.PasswordConfirmationValidation
|
||||
|
||||
metadata :token, :string do
|
||||
description "A JWT that can be used to authenticate the user."
|
||||
allow_nil? false
|
||||
end
|
||||
end
|
||||
|
||||
action :request_password_reset_token do
|
||||
description "Send password reset instructions to a user if they exist."
|
||||
|
||||
argument :email, :ci_string do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
# creates a reset token and invokes the relevant senders
|
||||
run {AshAuthentication.Strategy.Password.RequestPasswordReset, action: :get_by_email}
|
||||
end
|
||||
|
||||
read :get_by_email do
|
||||
description "Looks up a user by their email"
|
||||
get_by :email
|
||||
end
|
||||
|
||||
update :reset_password_with_token do
|
||||
argument :reset_token, :string do
|
||||
allow_nil? false
|
||||
sensitive? true
|
||||
end
|
||||
|
||||
argument :password, :string do
|
||||
description "The proposed password for the user, in plain text."
|
||||
allow_nil? false
|
||||
constraints min_length: 8
|
||||
sensitive? true
|
||||
end
|
||||
|
||||
argument :password_confirmation, :string do
|
||||
description "The proposed password for the user (again), in plain text."
|
||||
allow_nil? false
|
||||
sensitive? true
|
||||
end
|
||||
|
||||
# validates the provided reset token
|
||||
validate AshAuthentication.Strategy.Password.ResetTokenValidation
|
||||
|
||||
# validates that the password matches the confirmation
|
||||
validate AshAuthentication.Strategy.Password.PasswordConfirmationValidation
|
||||
|
||||
# Hashes the provided password
|
||||
change AshAuthentication.Strategy.Password.HashPasswordChange
|
||||
|
||||
# Generates an authentication token for the user
|
||||
change AshAuthentication.GenerateTokenChange
|
||||
end
|
||||
|
||||
create :sign_in_with_magic_link do
|
||||
description "Sign in or register a user with magic link."
|
||||
|
||||
argument :token, :string do
|
||||
description "The token from the magic link that was sent to the user"
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
argument :remember_me, :boolean do
|
||||
description "Whether to generate a remember me token"
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
upsert? true
|
||||
upsert_identity :unique_email
|
||||
upsert_fields [:email]
|
||||
|
||||
# Uses the information from the token to create or sign in the user
|
||||
change AshAuthentication.Strategy.MagicLink.SignInChange
|
||||
|
||||
change {AshAuthentication.Strategy.RememberMe.MaybeGenerateTokenChange,
|
||||
strategy_name: :remember_me}
|
||||
|
||||
metadata :token, :string do
|
||||
allow_nil? false
|
||||
end
|
||||
end
|
||||
|
||||
action :request_magic_link do
|
||||
argument :email, :ci_string do
|
||||
allow_nil? false
|
||||
end
|
||||
|
||||
run AshAuthentication.Strategy.MagicLink.Request
|
||||
end
|
||||
|
||||
read :sign_in_with_api_key do
|
||||
argument :api_key, :string, allow_nil?: false
|
||||
prepare AshAuthentication.Strategy.ApiKey.SignInPreparation
|
||||
end
|
||||
end
|
||||
|
||||
policies do
|
||||
bypass AshAuthentication.Checks.AshAuthenticationInteraction do
|
||||
authorize_if always()
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
attribute :email, :ci_string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :hashed_password, :string do
|
||||
sensitive? true
|
||||
end
|
||||
|
||||
attribute :confirmed_at, :utc_datetime_usec
|
||||
end
|
||||
|
||||
relationships do
|
||||
has_many :valid_api_keys, Mixer.Accounts.ApiKey do
|
||||
filter expr(valid)
|
||||
end
|
||||
end
|
||||
|
||||
identities do
|
||||
identity :unique_email, [:email]
|
||||
end
|
||||
end
|
||||
40
lib/mixer/accounts/user/senders/send_magic_link_email.ex
Normal file
40
lib/mixer/accounts/user/senders/send_magic_link_email.ex
Normal file
@@ -0,0 +1,40 @@
|
||||
defmodule Mixer.Accounts.User.Senders.SendMagicLinkEmail do
|
||||
@moduledoc """
|
||||
Sends a magic link email
|
||||
"""
|
||||
|
||||
use AshAuthentication.Sender
|
||||
use MixerWeb, :verified_routes
|
||||
|
||||
import Swoosh.Email
|
||||
alias Mixer.Mailer
|
||||
|
||||
@impl true
|
||||
def send(user_or_email, token, _) do
|
||||
# if you get a user, its for a user that already exists.
|
||||
# if you get an email, then the user does not yet exist.
|
||||
|
||||
email =
|
||||
case user_or_email do
|
||||
%{email: email} -> email
|
||||
email -> email
|
||||
end
|
||||
|
||||
new()
|
||||
# TODO: Replace with your email
|
||||
|> from({"noreply", "noreply@example.com"})
|
||||
|> to(to_string(email))
|
||||
|> subject("Your login link")
|
||||
|> html_body(body(token: token, email: email))
|
||||
|> Mailer.deliver!()
|
||||
end
|
||||
|
||||
defp body(params) do
|
||||
# NOTE: You may have to change this to match your magic link acceptance URL.
|
||||
|
||||
"""
|
||||
<p>Hello, #{params[:email]}! Click this link to sign in:</p>
|
||||
<p><a href="#{url(~p"/magic_link/#{params[:token]}")}">#{url(~p"/magic_link/#{params[:token]}")}</a></p>
|
||||
"""
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
defmodule Mixer.Accounts.User.Senders.SendNewUserConfirmationEmail do
|
||||
@moduledoc """
|
||||
Sends an email for a new user to confirm their email address.
|
||||
"""
|
||||
|
||||
use AshAuthentication.Sender
|
||||
use MixerWeb, :verified_routes
|
||||
|
||||
import Swoosh.Email
|
||||
|
||||
alias Mixer.Mailer
|
||||
|
||||
@impl true
|
||||
def send(user, token, _) do
|
||||
new()
|
||||
# TODO: Replace with your email
|
||||
|> from({"noreply", "noreply@example.com"})
|
||||
|> to(to_string(user.email))
|
||||
|> subject("Confirm your email address")
|
||||
|> html_body(body(token: token))
|
||||
|> Mailer.deliver!()
|
||||
end
|
||||
|
||||
defp body(params) do
|
||||
url = url(~p"/confirm_new_user/#{params[:token]}")
|
||||
|
||||
"""
|
||||
<p>Click this link to confirm your email:</p>
|
||||
<p><a href="#{url}">#{url}</a></p>
|
||||
"""
|
||||
end
|
||||
end
|
||||
32
lib/mixer/accounts/user/senders/send_password_reset_email.ex
Normal file
32
lib/mixer/accounts/user/senders/send_password_reset_email.ex
Normal file
@@ -0,0 +1,32 @@
|
||||
defmodule Mixer.Accounts.User.Senders.SendPasswordResetEmail do
|
||||
@moduledoc """
|
||||
Sends a password reset email
|
||||
"""
|
||||
|
||||
use AshAuthentication.Sender
|
||||
use MixerWeb, :verified_routes
|
||||
|
||||
import Swoosh.Email
|
||||
|
||||
alias Mixer.Mailer
|
||||
|
||||
@impl true
|
||||
def send(user, token, _) do
|
||||
new()
|
||||
# TODO: Replace with your email
|
||||
|> from({"noreply", "noreply@example.com"})
|
||||
|> to(to_string(user.email))
|
||||
|> subject("Reset your password")
|
||||
|> html_body(body(token: token))
|
||||
|> Mailer.deliver!()
|
||||
end
|
||||
|
||||
defp body(params) do
|
||||
url = url(~p"/password-reset/#{params[:token]}")
|
||||
|
||||
"""
|
||||
<p>Click this link to reset your password:</p>
|
||||
<p><a href="#{url}">#{url}</a></p>
|
||||
"""
|
||||
end
|
||||
end
|
||||
37
lib/mixer/application.ex
Normal file
37
lib/mixer/application.ex
Normal file
@@ -0,0 +1,37 @@
|
||||
defmodule Mixer.Application do
|
||||
# See https://hexdocs.pm/elixir/Application.html
|
||||
# for more information on OTP Applications
|
||||
@moduledoc false
|
||||
|
||||
use Application
|
||||
|
||||
@impl true
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
MixerWeb.Telemetry,
|
||||
Mixer.Repo,
|
||||
{DNSCluster, query: Application.get_env(:mixer, :dns_cluster_query) || :ignore},
|
||||
{Phoenix.PubSub, name: Mixer.PubSub},
|
||||
# Start a worker by calling: Mixer.Worker.start_link(arg)
|
||||
# {Mixer.Worker, arg},
|
||||
# Start to serve requests, typically the last entry
|
||||
MixerWeb.Endpoint,
|
||||
{Absinthe.Subscription, MixerWeb.Endpoint},
|
||||
AshGraphql.Subscription.Batcher,
|
||||
{AshAuthentication.Supervisor, [otp_app: :mixer]}
|
||||
]
|
||||
|
||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||
# for other strategies and supported options
|
||||
opts = [strategy: :one_for_one, name: Mixer.Supervisor]
|
||||
Supervisor.start_link(children, opts)
|
||||
end
|
||||
|
||||
# Tell Phoenix to update the endpoint configuration
|
||||
# whenever the application is updated.
|
||||
@impl true
|
||||
def config_change(changed, _new, removed) do
|
||||
MixerWeb.Endpoint.config_change(changed, removed)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
3
lib/mixer/mailer.ex
Normal file
3
lib/mixer/mailer.ex
Normal file
@@ -0,0 +1,3 @@
|
||||
defmodule Mixer.Mailer do
|
||||
use Swoosh.Mailer, otp_app: :mixer
|
||||
end
|
||||
22
lib/mixer/repo.ex
Normal file
22
lib/mixer/repo.ex
Normal file
@@ -0,0 +1,22 @@
|
||||
defmodule Mixer.Repo do
|
||||
use AshPostgres.Repo,
|
||||
otp_app: :mixer
|
||||
|
||||
@impl true
|
||||
def installed_extensions do
|
||||
# Add extensions here, and the migration generator will install them.
|
||||
["ash-functions", "citext"]
|
||||
end
|
||||
|
||||
# Don't open unnecessary transactions
|
||||
# will default to `false` in 4.0
|
||||
@impl true
|
||||
def prefer_transaction? do
|
||||
false
|
||||
end
|
||||
|
||||
@impl true
|
||||
def min_pg_version do
|
||||
%Version{major: 18, minor: 3, patch: 0}
|
||||
end
|
||||
end
|
||||
12
lib/mixer/secrets.ex
Normal file
12
lib/mixer/secrets.ex
Normal file
@@ -0,0 +1,12 @@
|
||||
defmodule Mixer.Secrets do
|
||||
use AshAuthentication.Secret
|
||||
|
||||
def secret_for(
|
||||
[:authentication, :tokens, :signing_secret],
|
||||
Mixer.Accounts.User,
|
||||
_opts,
|
||||
_context
|
||||
) do
|
||||
Application.fetch_env(:mixer, :token_signing_secret)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user