add tests
This commit is contained in:
parent
86960c2709
commit
4cf90d78a9
|
@ -59,6 +59,8 @@ COPY ./services/bright/lib lib
|
||||||
|
|
||||||
COPY ./services/bright/assets assets
|
COPY ./services/bright/assets assets
|
||||||
|
|
||||||
|
COPY ./services/bright/test test
|
||||||
|
|
||||||
|
|
||||||
# compile assets
|
# compile assets
|
||||||
RUN mix assets.deploy
|
RUN mix assets.deploy
|
||||||
|
@ -75,7 +77,7 @@ RUN mix release
|
||||||
|
|
||||||
## dev target
|
## dev target
|
||||||
FROM builder AS dev
|
FROM builder AS dev
|
||||||
RUN echo "balls. that is all."
|
COPY ./services/bright/config/test.exs config/test.exs
|
||||||
RUN ls -la ./contrib/
|
RUN ls -la ./contrib/
|
||||||
CMD [ "mix", "phx.server" ]
|
CMD [ "mix", "phx.server" ]
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,8 @@ config :bright,
|
||||||
ecto_repos: [Bright.Repo],
|
ecto_repos: [Bright.Repo],
|
||||||
generators: [timestamp_type: :utc_datetime]
|
generators: [timestamp_type: :utc_datetime]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Configures the endpoint
|
# Configures the endpoint
|
||||||
config :bright, BrightWeb.Endpoint,
|
config :bright, BrightWeb.Endpoint,
|
||||||
url: [host: "localhost"],
|
url: [host: "localhost"],
|
||||||
|
@ -40,6 +42,13 @@ config :ueberauth, Ueberauth,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# These variables are required at runtime, but we must get them from system env here (not sure why)
|
||||||
|
# we don't raise here, in case mix is running some task like creating an ecto migration
|
||||||
|
config :ueberauth, Ueberauth.Strategy.Github.OAuth,
|
||||||
|
client_id: System.get_env("GITHUB_CLIENT_ID"),
|
||||||
|
client_secret: System.get_env("GITHUB_CLIENT_SECRET")
|
||||||
|
|
||||||
|
|
||||||
# Configures the mailer
|
# Configures the mailer
|
||||||
#
|
#
|
||||||
# By default it uses the "Local" adapter which stores the emails
|
# By default it uses the "Local" adapter which stores the emails
|
||||||
|
|
|
@ -20,6 +20,28 @@ if System.get_env("PHX_SERVER") do
|
||||||
config :bright, BrightWeb.Endpoint, server: true
|
config :bright, BrightWeb.Endpoint, server: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
config :bright,
|
||||||
|
aws_bucket: System.get_env("AWS_BUCKET"),
|
||||||
|
aws_host: System.get_env("AWS_HOST"),
|
||||||
|
aws_access_key_id: System.get_env("AWS_ACCESS_KEY_ID"),
|
||||||
|
aws_secret_access_key: System.get_env("AWS_SECRET_ACCESS_KEY"),
|
||||||
|
aws_region: System.get_env("AWS_REGION"),
|
||||||
|
superstreamer_url: System.get_env("SUPERSTREAMER_URL"),
|
||||||
|
superstreamer_auth_token: System.get_env("SUPERSTREAMER_AUTH_TOKEN"),
|
||||||
|
public_s3_endpoint: System.get_env("PUBLIC_S3_ENDPOINT"),
|
||||||
|
s3_cdn_endpoint: System.get_env("PUBLIC_S3_ENDPOINT")
|
||||||
|
|
||||||
|
|
||||||
|
# @see https://elixirforum.com/t/backblaze-and-ex-aws-ex-aws-s3-2-4-3-presign-url-issue/56805
|
||||||
|
config :ex_aws,
|
||||||
|
access_key_id: {:system, "AWS_ACCESS_KEY_ID"},
|
||||||
|
secret_access_key: {:system, "AWS_SECRET_ACCESS_KEY"},
|
||||||
|
s3: [
|
||||||
|
host: System.get_env("AWS_HOST"),
|
||||||
|
bucket: System.get_env("AWS_BUCKET")
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
if config_env() == :prod do
|
if config_env() == :prod do
|
||||||
database_url =
|
database_url =
|
||||||
System.get_env("DATABASE_URL") ||
|
System.get_env("DATABASE_URL") ||
|
||||||
|
@ -51,6 +73,7 @@ if config_env() == :prod do
|
||||||
host = System.get_env("PHX_HOST") || "example.com"
|
host = System.get_env("PHX_HOST") || "example.com"
|
||||||
port = String.to_integer(System.get_env("PORT") || "4000")
|
port = String.to_integer(System.get_env("PORT") || "4000")
|
||||||
|
|
||||||
|
|
||||||
config :bright, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
|
config :bright, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
|
||||||
|
|
||||||
config :bright, BrightWeb.Endpoint,
|
config :bright, BrightWeb.Endpoint,
|
||||||
|
@ -65,18 +88,23 @@ if config_env() == :prod do
|
||||||
],
|
],
|
||||||
secret_key_base: secret_key_base
|
secret_key_base: secret_key_base
|
||||||
|
|
||||||
config :ueberauth, Ueberauth.Strategy.Github.OAuth,
|
|
||||||
client_id: System.get_env("GITHUB_CLIENT_ID") || raise("environment variable GITHUB_CLIENT_ID is missing."),
|
|
||||||
client_secret: System.get_env("GITHUB_CLIENT_SECRET") || raise("environment variable GITHUB_CLIENT_SECRET is missing.")
|
|
||||||
|
# We need to stop the program from running if OAuth client IDs and client secrets are not present in env.
|
||||||
|
# We also do this in config.exs, but we wait to raise until here otherwise mix wouldn't be able to run ecto migrations
|
||||||
|
System.get_env("GITHUB_CLIENT_ID") || raise("environment variable GITHUB_CLIENT_ID is missing.")
|
||||||
|
System.get_env("GITHUB_CLIENT_SECRET") || raise("environment variable GITHUB_CLIENT_SECRET is missing.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# config :ueberauth, Ueberauth.Strategy.Patreon.OAuth,
|
# config :ueberauth, Ueberauth.Strategy.Patreon.OAuth,
|
||||||
# client_id: System.get_env("PATREON_CLIENT_ID"),
|
# client_id: System.get_env("PATREON_CLIENT_ID"),
|
||||||
# client_secret: System.get_env("PATREON_CLIENT_SECRET")
|
# client_secret: System.get_env("PATREON_CLIENT_SECRET")
|
||||||
|
|
||||||
# config :ueberauth, Ueberauth.Strategy.Github.OAuth,
|
|
||||||
# client_id: System.get_env("GITHUB_CLIENT_ID"),
|
|
||||||
# client_secret: System.get_env("GITHUB_CLIENT_SECRET")
|
|
||||||
|
|
||||||
# config :ueberauth, Ueberauth.Strategy.Github.OAuth,
|
# config :ueberauth, Ueberauth.Strategy.Github.OAuth,
|
||||||
# client_id: {:system, "GITHUB_CLIENT_ID"},
|
# client_id: {:system, "GITHUB_CLIENT_ID"},
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import Config
|
import Config
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Only in tests, remove the complexity from the password hashing algorithm
|
# Only in tests, remove the complexity from the password hashing algorithm
|
||||||
config :bcrypt_elixir, :log_rounds, 1
|
config :bcrypt_elixir, :log_rounds, 1
|
||||||
|
|
||||||
|
@ -9,7 +12,9 @@ config :bcrypt_elixir, :log_rounds, 1
|
||||||
# to provide built-in test partitioning in CI environment.
|
# to provide built-in test partitioning in CI environment.
|
||||||
# Run `mix help test` for more information.
|
# Run `mix help test` for more information.
|
||||||
config :bright, Bright.Repo,
|
config :bright, Bright.Repo,
|
||||||
url: "#{System.get_env("DATABASE_URL")}",
|
username: "postgres",
|
||||||
|
password: "password",
|
||||||
|
hostname: System.cmd("docker", ["inspect", "--format", "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}", "futureporn-db"]) |> elem(0) |> String.trim(),
|
||||||
database: "bright_test#{System.get_env("MIX_TEST_PARTITION")}",
|
database: "bright_test#{System.get_env("MIX_TEST_PARTITION")}",
|
||||||
pool: Ecto.Adapters.SQL.Sandbox,
|
pool: Ecto.Adapters.SQL.Sandbox,
|
||||||
pool_size: System.schedulers_online() * 2
|
pool_size: System.schedulers_online() * 2
|
||||||
|
@ -22,7 +27,8 @@ config :bright, BrightWeb.Endpoint,
|
||||||
server: false
|
server: false
|
||||||
|
|
||||||
# Prevent Oban from running jobs and plugins during test runs
|
# Prevent Oban from running jobs and plugins during test runs
|
||||||
config :bright, Oban, testing: :inline
|
config :bright, Oban,
|
||||||
|
testing: :manual
|
||||||
|
|
||||||
# Have Superstreamer use mocks during testing
|
# Have Superstreamer use mocks during testing
|
||||||
config :bright, superstreamer_api_client: ApiClientBehaviorMock
|
config :bright, superstreamer_api_client: ApiClientBehaviorMock
|
||||||
|
@ -42,3 +48,10 @@ config :phoenix, :plug_init_mode, :runtime
|
||||||
# Enable helpful, but potentially expensive runtime checks
|
# Enable helpful, but potentially expensive runtime checks
|
||||||
config :phoenix_live_view,
|
config :phoenix_live_view,
|
||||||
enable_expensive_runtime_checks: true
|
enable_expensive_runtime_checks: true
|
||||||
|
|
||||||
|
# @see https://elixirforum.com/t/backblaze-and-ex-aws-ex-aws-s3-2-4-3-presign-url-issue/56805
|
||||||
|
# config :ex_aws, :s3,
|
||||||
|
# host: Application.get_env(:bright, :aws_host),
|
||||||
|
# access_key_id: Application.get_env(:bright, :aws_access_key_id),
|
||||||
|
# secret_access_key: Application.get_env(:bright, :aws_secret_access_key),
|
||||||
|
# bucket: Application.get_env(:bright, :aws_bucket)
|
||||||
|
|
|
@ -1,357 +0,0 @@
|
||||||
defmodule Bright.Accounts do
|
|
||||||
@moduledoc """
|
|
||||||
The Accounts context.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import Ecto.Query, warn: false
|
|
||||||
alias Bright.Repo
|
|
||||||
|
|
||||||
alias Bright.Accounts.{User, UserToken, UserNotifier}
|
|
||||||
|
|
||||||
## Database getters
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Gets a user by email.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> get_user_by_email("foo@example.com")
|
|
||||||
%User{}
|
|
||||||
|
|
||||||
iex> get_user_by_email("unknown@example.com")
|
|
||||||
nil
|
|
||||||
|
|
||||||
"""
|
|
||||||
def get_user_by_email(email) when is_binary(email) do
|
|
||||||
Repo.get_by(User, email: email)
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Gets a user by email and password.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> get_user_by_email_and_password("foo@example.com", "correct_password")
|
|
||||||
%User{}
|
|
||||||
|
|
||||||
iex> get_user_by_email_and_password("foo@example.com", "invalid_password")
|
|
||||||
nil
|
|
||||||
|
|
||||||
"""
|
|
||||||
def get_user_by_email_and_password(email, password)
|
|
||||||
when is_binary(email) and is_binary(password) do
|
|
||||||
user = Repo.get_by(User, email: email)
|
|
||||||
if User.valid_password?(user, password), do: user
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Gets a single user.
|
|
||||||
|
|
||||||
Raises `Ecto.NoResultsError` if the User does not exist.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> get_user!(123)
|
|
||||||
%User{}
|
|
||||||
|
|
||||||
iex> get_user!(456)
|
|
||||||
** (Ecto.NoResultsError)
|
|
||||||
|
|
||||||
"""
|
|
||||||
def get_user!(id), do: Repo.get!(User, id)
|
|
||||||
|
|
||||||
## User registration
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Registers a user.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> register_user(%{field: value})
|
|
||||||
{:ok, %User{}}
|
|
||||||
|
|
||||||
iex> register_user(%{field: bad_value})
|
|
||||||
{:error, %Ecto.Changeset{}}
|
|
||||||
|
|
||||||
"""
|
|
||||||
def register_user(attrs) do
|
|
||||||
%User{}
|
|
||||||
|> User.registration_changeset(attrs)
|
|
||||||
|> Repo.insert()
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Returns an `%Ecto.Changeset{}` for tracking user changes.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> change_user_registration(user)
|
|
||||||
%Ecto.Changeset{data: %User{}}
|
|
||||||
|
|
||||||
"""
|
|
||||||
def change_user_registration(%User{} = user, attrs \\ %{}) do
|
|
||||||
User.registration_changeset(user, attrs, hash_password: false, validate_email: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
## Settings
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Returns an `%Ecto.Changeset{}` for changing the user email.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> change_user_email(user)
|
|
||||||
%Ecto.Changeset{data: %User{}}
|
|
||||||
|
|
||||||
"""
|
|
||||||
def change_user_email(user, attrs \\ %{}) do
|
|
||||||
User.email_changeset(user, attrs, validate_email: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Emulates that the email will change without actually changing
|
|
||||||
it in the database.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> apply_user_email(user, "valid password", %{email: ...})
|
|
||||||
{:ok, %User{}}
|
|
||||||
|
|
||||||
iex> apply_user_email(user, "invalid password", %{email: ...})
|
|
||||||
{:error, %Ecto.Changeset{}}
|
|
||||||
|
|
||||||
"""
|
|
||||||
def apply_user_email(user, password, attrs) do
|
|
||||||
user
|
|
||||||
|> User.email_changeset(attrs)
|
|
||||||
|> User.validate_current_password(password)
|
|
||||||
|> Ecto.Changeset.apply_action(:update)
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Updates the user email using the given token.
|
|
||||||
|
|
||||||
If the token matches, the user email is updated and the token is deleted.
|
|
||||||
The confirmed_at date is also updated to the current time.
|
|
||||||
"""
|
|
||||||
def update_user_email(user, token) do
|
|
||||||
context = "change:#{user.email}"
|
|
||||||
|
|
||||||
with {:ok, query} <- UserToken.verify_change_email_token_query(token, context),
|
|
||||||
%UserToken{sent_to: email} <- Repo.one(query),
|
|
||||||
{:ok, _} <- Repo.transaction(user_email_multi(user, email, context)) do
|
|
||||||
:ok
|
|
||||||
else
|
|
||||||
_ -> :error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp user_email_multi(user, email, context) do
|
|
||||||
changeset =
|
|
||||||
user
|
|
||||||
|> User.email_changeset(%{email: email})
|
|
||||||
|> User.confirm_changeset()
|
|
||||||
|
|
||||||
Ecto.Multi.new()
|
|
||||||
|> Ecto.Multi.update(:user, changeset)
|
|
||||||
|> Ecto.Multi.delete_all(:tokens, UserToken.by_user_and_contexts_query(user, [context]))
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@doc ~S"""
|
|
||||||
Delivers the update email instructions to the given user.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> deliver_user_update_email_instructions(user, current_email, &url(~p"/users/settings/confirm_email/#{&1}"))
|
|
||||||
{:ok, %{to: ..., body: ...}}
|
|
||||||
|
|
||||||
"""
|
|
||||||
def deliver_user_update_email_instructions(%User{} = user, current_email, update_email_url_fun)
|
|
||||||
when is_function(update_email_url_fun, 1) do
|
|
||||||
{encoded_token, user_token} = UserToken.build_email_token(user, "change:#{current_email}")
|
|
||||||
|
|
||||||
Repo.insert!(user_token)
|
|
||||||
UserNotifier.deliver_update_email_instructions(user, update_email_url_fun.(encoded_token))
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Returns an `%Ecto.Changeset{}` for changing the user password.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> change_user_password(user)
|
|
||||||
%Ecto.Changeset{data: %User{}}
|
|
||||||
|
|
||||||
"""
|
|
||||||
def change_user_password(user, attrs \\ %{}) do
|
|
||||||
User.password_changeset(user, attrs, hash_password: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Updates the user password.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> update_user_password(user, "valid password", %{password: ...})
|
|
||||||
{:ok, %User{}}
|
|
||||||
|
|
||||||
iex> update_user_password(user, "invalid password", %{password: ...})
|
|
||||||
{:error, %Ecto.Changeset{}}
|
|
||||||
|
|
||||||
"""
|
|
||||||
def update_user_password(user, password, attrs) do
|
|
||||||
changeset =
|
|
||||||
user
|
|
||||||
|> User.password_changeset(attrs)
|
|
||||||
|> User.validate_current_password(password)
|
|
||||||
|
|
||||||
Ecto.Multi.new()
|
|
||||||
|> Ecto.Multi.update(:user, changeset)
|
|
||||||
|> Ecto.Multi.delete_all(:tokens, UserToken.by_user_and_contexts_query(user, :all))
|
|
||||||
|> Repo.transaction()
|
|
||||||
|> case do
|
|
||||||
{:ok, %{user: user}} -> {:ok, user}
|
|
||||||
{:error, :user, changeset, _} -> {:error, changeset}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
## Session
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Generates a session token.
|
|
||||||
"""
|
|
||||||
def generate_user_session_token(user) do
|
|
||||||
{token, user_token} = UserToken.build_session_token(user)
|
|
||||||
Repo.insert!(user_token)
|
|
||||||
token
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Gets the user with the given signed token.
|
|
||||||
"""
|
|
||||||
def get_user_by_session_token(token) do
|
|
||||||
{:ok, query} = UserToken.verify_session_token_query(token)
|
|
||||||
Repo.one(query)
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Deletes the signed token with the given context.
|
|
||||||
"""
|
|
||||||
def delete_user_session_token(token) do
|
|
||||||
Repo.delete_all(UserToken.by_token_and_context_query(token, "session"))
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
|
|
||||||
## Confirmation
|
|
||||||
|
|
||||||
@doc ~S"""
|
|
||||||
Delivers the confirmation email instructions to the given user.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> deliver_user_confirmation_instructions(user, &url(~p"/users/confirm/#{&1}"))
|
|
||||||
{:ok, %{to: ..., body: ...}}
|
|
||||||
|
|
||||||
iex> deliver_user_confirmation_instructions(confirmed_user, &url(~p"/users/confirm/#{&1}"))
|
|
||||||
{:error, :already_confirmed}
|
|
||||||
|
|
||||||
"""
|
|
||||||
def deliver_user_confirmation_instructions(%User{} = user, confirmation_url_fun)
|
|
||||||
when is_function(confirmation_url_fun, 1) do
|
|
||||||
if user.confirmed_at do
|
|
||||||
{:error, :already_confirmed}
|
|
||||||
else
|
|
||||||
{encoded_token, user_token} = UserToken.build_email_token(user, "confirm")
|
|
||||||
Repo.insert!(user_token)
|
|
||||||
UserNotifier.deliver_confirmation_instructions(user, confirmation_url_fun.(encoded_token))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Confirms a user by the given token.
|
|
||||||
|
|
||||||
If the token matches, the user account is marked as confirmed
|
|
||||||
and the token is deleted.
|
|
||||||
"""
|
|
||||||
def confirm_user(token) do
|
|
||||||
with {:ok, query} <- UserToken.verify_email_token_query(token, "confirm"),
|
|
||||||
%User{} = user <- Repo.one(query),
|
|
||||||
{:ok, %{user: user}} <- Repo.transaction(confirm_user_multi(user)) do
|
|
||||||
{:ok, user}
|
|
||||||
else
|
|
||||||
_ -> :error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp confirm_user_multi(user) do
|
|
||||||
Ecto.Multi.new()
|
|
||||||
|> Ecto.Multi.update(:user, User.confirm_changeset(user))
|
|
||||||
|> Ecto.Multi.delete_all(:tokens, UserToken.by_user_and_contexts_query(user, ["confirm"]))
|
|
||||||
end
|
|
||||||
|
|
||||||
## Reset password
|
|
||||||
|
|
||||||
@doc ~S"""
|
|
||||||
Delivers the reset password email to the given user.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> deliver_user_reset_password_instructions(user, &url(~p"/users/reset_password/#{&1}"))
|
|
||||||
{:ok, %{to: ..., body: ...}}
|
|
||||||
|
|
||||||
"""
|
|
||||||
def deliver_user_reset_password_instructions(%User{} = user, reset_password_url_fun)
|
|
||||||
when is_function(reset_password_url_fun, 1) do
|
|
||||||
{encoded_token, user_token} = UserToken.build_email_token(user, "reset_password")
|
|
||||||
Repo.insert!(user_token)
|
|
||||||
UserNotifier.deliver_reset_password_instructions(user, reset_password_url_fun.(encoded_token))
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Gets the user by reset password token.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> get_user_by_reset_password_token("validtoken")
|
|
||||||
%User{}
|
|
||||||
|
|
||||||
iex> get_user_by_reset_password_token("invalidtoken")
|
|
||||||
nil
|
|
||||||
|
|
||||||
"""
|
|
||||||
def get_user_by_reset_password_token(token) do
|
|
||||||
with {:ok, query} <- UserToken.verify_email_token_query(token, "reset_password"),
|
|
||||||
%User{} = user <- Repo.one(query) do
|
|
||||||
user
|
|
||||||
else
|
|
||||||
_ -> nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Resets the user password.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> reset_user_password(user, %{password: "new long password", password_confirmation: "new long password"})
|
|
||||||
{:ok, %User{}}
|
|
||||||
|
|
||||||
iex> reset_user_password(user, %{password: "valid", password_confirmation: "not the same"})
|
|
||||||
{:error, %Ecto.Changeset{}}
|
|
||||||
|
|
||||||
"""
|
|
||||||
def reset_user_password(user, attrs) do
|
|
||||||
Ecto.Multi.new()
|
|
||||||
|> Ecto.Multi.update(:user, User.password_changeset(user, attrs))
|
|
||||||
|> Ecto.Multi.delete_all(:tokens, UserToken.by_user_and_contexts_query(user, :all))
|
|
||||||
|> Repo.transaction()
|
|
||||||
|> case do
|
|
||||||
{:ok, %{user: user}} -> {:ok, user}
|
|
||||||
{:error, :user, changeset, _} -> {:error, changeset}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,162 +0,0 @@
|
||||||
defmodule Bright.Accounts.User do
|
|
||||||
use Ecto.Schema
|
|
||||||
import Ecto.Changeset
|
|
||||||
|
|
||||||
schema "users" do
|
|
||||||
field :email, :string
|
|
||||||
field :password, :string, virtual: true, redact: true
|
|
||||||
field :hashed_password, :string, redact: true
|
|
||||||
field :current_password, :string, virtual: true, redact: true
|
|
||||||
field :confirmed_at, :utc_datetime
|
|
||||||
field :provider, :string
|
|
||||||
|
|
||||||
timestamps(type: :utc_datetime)
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
A user changeset for registration.
|
|
||||||
|
|
||||||
It is important to validate the length of both email and password.
|
|
||||||
Otherwise databases may truncate the email without warnings, which
|
|
||||||
could lead to unpredictable or insecure behaviour. Long passwords may
|
|
||||||
also be very expensive to hash for certain algorithms.
|
|
||||||
|
|
||||||
## Options
|
|
||||||
|
|
||||||
* `:hash_password` - Hashes the password so it can be stored securely
|
|
||||||
in the database and ensures the password field is cleared to prevent
|
|
||||||
leaks in the logs. If password hashing is not needed and clearing the
|
|
||||||
password field is not desired (like when using this changeset for
|
|
||||||
validations on a LiveView form), this option can be set to `false`.
|
|
||||||
Defaults to `true`.
|
|
||||||
|
|
||||||
* `:validate_email` - Validates the uniqueness of the email, in case
|
|
||||||
you don't want to validate the uniqueness of the email (like when
|
|
||||||
using this changeset for validations on a LiveView form before
|
|
||||||
submitting the form), this option can be set to `false`.
|
|
||||||
Defaults to `true`.
|
|
||||||
"""
|
|
||||||
def registration_changeset(user, attrs, opts \\ []) do
|
|
||||||
user
|
|
||||||
|> cast(attrs, [:email, :password])
|
|
||||||
|> validate_email(opts)
|
|
||||||
|> validate_password(opts)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp validate_email(changeset, opts) do
|
|
||||||
changeset
|
|
||||||
|> validate_required([:email, :provider])
|
|
||||||
|> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must have the @ sign and no spaces")
|
|
||||||
|> validate_length(:email, max: 160)
|
|
||||||
|> maybe_validate_unique_email(opts)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp validate_password(changeset, opts) do
|
|
||||||
changeset
|
|
||||||
|> validate_required([:password])
|
|
||||||
|> validate_length(:password, min: 12, max: 72)
|
|
||||||
# Examples of additional password validation:
|
|
||||||
# |> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character")
|
|
||||||
# |> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character")
|
|
||||||
# |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character")
|
|
||||||
|> maybe_hash_password(opts)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp maybe_hash_password(changeset, opts) do
|
|
||||||
hash_password? = Keyword.get(opts, :hash_password, true)
|
|
||||||
password = get_change(changeset, :password)
|
|
||||||
|
|
||||||
if hash_password? && password && changeset.valid? do
|
|
||||||
changeset
|
|
||||||
# If using Bcrypt, then further validate it is at most 72 bytes long
|
|
||||||
|> validate_length(:password, max: 72, count: :bytes)
|
|
||||||
# Hashing could be done with `Ecto.Changeset.prepare_changes/2`, but that
|
|
||||||
# would keep the database transaction open longer and hurt performance.
|
|
||||||
|> put_change(:hashed_password, Bcrypt.hash_pwd_salt(password))
|
|
||||||
|> delete_change(:password)
|
|
||||||
else
|
|
||||||
changeset
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp maybe_validate_unique_email(changeset, opts) do
|
|
||||||
if Keyword.get(opts, :validate_email, true) do
|
|
||||||
changeset
|
|
||||||
|> unsafe_validate_unique(:email, Bright.Repo)
|
|
||||||
|> unique_constraint(:email)
|
|
||||||
else
|
|
||||||
changeset
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
A user changeset for changing the email.
|
|
||||||
|
|
||||||
It requires the email to change otherwise an error is added.
|
|
||||||
"""
|
|
||||||
def email_changeset(user, attrs, opts \\ []) do
|
|
||||||
user
|
|
||||||
|> cast(attrs, [:email])
|
|
||||||
|> validate_email(opts)
|
|
||||||
|> case do
|
|
||||||
%{changes: %{email: _}} = changeset -> changeset
|
|
||||||
%{} = changeset -> add_error(changeset, :email, "did not change")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
A user changeset for changing the password.
|
|
||||||
|
|
||||||
## Options
|
|
||||||
|
|
||||||
* `:hash_password` - Hashes the password so it can be stored securely
|
|
||||||
in the database and ensures the password field is cleared to prevent
|
|
||||||
leaks in the logs. If password hashing is not needed and clearing the
|
|
||||||
password field is not desired (like when using this changeset for
|
|
||||||
validations on a LiveView form), this option can be set to `false`.
|
|
||||||
Defaults to `true`.
|
|
||||||
"""
|
|
||||||
def password_changeset(user, attrs, opts \\ []) do
|
|
||||||
user
|
|
||||||
|> cast(attrs, [:password])
|
|
||||||
|> validate_confirmation(:password, message: "does not match password")
|
|
||||||
|> validate_password(opts)
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Confirms the account by setting `confirmed_at`.
|
|
||||||
"""
|
|
||||||
def confirm_changeset(user) do
|
|
||||||
now = DateTime.utc_now() |> DateTime.truncate(:second)
|
|
||||||
change(user, confirmed_at: now)
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Verifies the password.
|
|
||||||
|
|
||||||
If there is no user or the user doesn't have a password, we call
|
|
||||||
`Bcrypt.no_user_verify/0` to avoid timing attacks.
|
|
||||||
"""
|
|
||||||
def valid_password?(%Bright.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
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Validates the current password otherwise adds an error to the changeset.
|
|
||||||
"""
|
|
||||||
def validate_current_password(changeset, password) do
|
|
||||||
changeset = cast(changeset, %{current_password: password}, [:current_password])
|
|
||||||
|
|
||||||
if valid_password?(changeset.data, password) do
|
|
||||||
changeset
|
|
||||||
else
|
|
||||||
add_error(changeset, :current_password, "is not valid")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,79 +0,0 @@
|
||||||
defmodule Bright.Accounts.UserNotifier do
|
|
||||||
import Swoosh.Email
|
|
||||||
|
|
||||||
alias Bright.Mailer
|
|
||||||
|
|
||||||
# Delivers the email using the application mailer.
|
|
||||||
defp deliver(recipient, subject, body) do
|
|
||||||
email =
|
|
||||||
new()
|
|
||||||
|> to(recipient)
|
|
||||||
|> from({"Bright", "contact@example.com"})
|
|
||||||
|> subject(subject)
|
|
||||||
|> text_body(body)
|
|
||||||
|
|
||||||
with {:ok, _metadata} <- Mailer.deliver(email) do
|
|
||||||
{:ok, email}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Deliver instructions to confirm account.
|
|
||||||
"""
|
|
||||||
def deliver_confirmation_instructions(user, url) do
|
|
||||||
deliver(user.email, "Confirmation instructions", """
|
|
||||||
|
|
||||||
==============================
|
|
||||||
|
|
||||||
Hi #{user.email},
|
|
||||||
|
|
||||||
You can confirm your account by visiting the URL below:
|
|
||||||
|
|
||||||
#{url}
|
|
||||||
|
|
||||||
If you didn't create an account with us, please ignore this.
|
|
||||||
|
|
||||||
==============================
|
|
||||||
""")
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Deliver instructions to reset a user password.
|
|
||||||
"""
|
|
||||||
def deliver_reset_password_instructions(user, url) do
|
|
||||||
deliver(user.email, "Reset password instructions", """
|
|
||||||
|
|
||||||
==============================
|
|
||||||
|
|
||||||
Hi #{user.email},
|
|
||||||
|
|
||||||
You can reset your password by visiting the URL below:
|
|
||||||
|
|
||||||
#{url}
|
|
||||||
|
|
||||||
If you didn't request this change, please ignore this.
|
|
||||||
|
|
||||||
==============================
|
|
||||||
""")
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Deliver instructions to update a user email.
|
|
||||||
"""
|
|
||||||
def deliver_update_email_instructions(user, url) do
|
|
||||||
deliver(user.email, "Update email instructions", """
|
|
||||||
|
|
||||||
==============================
|
|
||||||
|
|
||||||
Hi #{user.email},
|
|
||||||
|
|
||||||
You can change your email by visiting the URL below:
|
|
||||||
|
|
||||||
#{url}
|
|
||||||
|
|
||||||
If you didn't request this change, please ignore this.
|
|
||||||
|
|
||||||
==============================
|
|
||||||
""")
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,179 +0,0 @@
|
||||||
defmodule Bright.Accounts.UserToken do
|
|
||||||
use Ecto.Schema
|
|
||||||
import Ecto.Query
|
|
||||||
alias Bright.Accounts.UserToken
|
|
||||||
|
|
||||||
@hash_algorithm :sha256
|
|
||||||
@rand_size 32
|
|
||||||
|
|
||||||
# It is very important to keep the reset password token expiry short,
|
|
||||||
# since someone with access to the email may take over the account.
|
|
||||||
@reset_password_validity_in_days 1
|
|
||||||
@confirm_validity_in_days 7
|
|
||||||
@change_email_validity_in_days 7
|
|
||||||
@session_validity_in_days 60
|
|
||||||
|
|
||||||
schema "users_tokens" do
|
|
||||||
field :token, :binary
|
|
||||||
field :context, :string
|
|
||||||
field :sent_to, :string
|
|
||||||
belongs_to :user, Bright.Accounts.User
|
|
||||||
|
|
||||||
timestamps(type: :utc_datetime, updated_at: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Generates a token that will be stored in a signed place,
|
|
||||||
such as session or cookie. As they are signed, those
|
|
||||||
tokens do not need to be hashed.
|
|
||||||
|
|
||||||
The reason why we store session tokens in the database, even
|
|
||||||
though Phoenix already provides a session cookie, is because
|
|
||||||
Phoenix' default session cookies are not persisted, they are
|
|
||||||
simply signed and potentially encrypted. This means they are
|
|
||||||
valid indefinitely, unless you change the signing/encryption
|
|
||||||
salt.
|
|
||||||
|
|
||||||
Therefore, storing them allows individual user
|
|
||||||
sessions to be expired. The token system can also be extended
|
|
||||||
to store additional data, such as the device used for logging in.
|
|
||||||
You could then use this information to display all valid sessions
|
|
||||||
and devices in the UI and allow users to explicitly expire any
|
|
||||||
session they deem invalid.
|
|
||||||
"""
|
|
||||||
def build_session_token(user) do
|
|
||||||
token = :crypto.strong_rand_bytes(@rand_size)
|
|
||||||
{token, %UserToken{token: token, context: "session", user_id: user.id}}
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Checks if the token is valid and returns its underlying lookup query.
|
|
||||||
|
|
||||||
The query returns the user found by the token, if any.
|
|
||||||
|
|
||||||
The token is valid if it matches the value in the database and it has
|
|
||||||
not expired (after @session_validity_in_days).
|
|
||||||
"""
|
|
||||||
def verify_session_token_query(token) do
|
|
||||||
query =
|
|
||||||
from token in by_token_and_context_query(token, "session"),
|
|
||||||
join: user in assoc(token, :user),
|
|
||||||
where: token.inserted_at > ago(@session_validity_in_days, "day"),
|
|
||||||
select: user
|
|
||||||
|
|
||||||
{:ok, query}
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Builds a token and its hash to be delivered to the user's email.
|
|
||||||
|
|
||||||
The non-hashed token is sent to the user email while the
|
|
||||||
hashed part is stored in the database. The original token cannot be reconstructed,
|
|
||||||
which means anyone with read-only access to the database cannot directly use
|
|
||||||
the token in the application to gain access. Furthermore, if the user changes
|
|
||||||
their email in the system, the tokens sent to the previous email are no longer
|
|
||||||
valid.
|
|
||||||
|
|
||||||
Users can easily adapt the existing code to provide other types of delivery methods,
|
|
||||||
for example, by phone numbers.
|
|
||||||
"""
|
|
||||||
def build_email_token(user, context) do
|
|
||||||
build_hashed_token(user, context, user.email)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp build_hashed_token(user, context, sent_to) do
|
|
||||||
token = :crypto.strong_rand_bytes(@rand_size)
|
|
||||||
hashed_token = :crypto.hash(@hash_algorithm, token)
|
|
||||||
|
|
||||||
{Base.url_encode64(token, padding: false),
|
|
||||||
%UserToken{
|
|
||||||
token: hashed_token,
|
|
||||||
context: context,
|
|
||||||
sent_to: sent_to,
|
|
||||||
user_id: user.id
|
|
||||||
}}
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Checks if the token is valid and returns its underlying lookup query.
|
|
||||||
|
|
||||||
The query returns the user found by the token, if any.
|
|
||||||
|
|
||||||
The given token is valid if it matches its hashed counterpart in the
|
|
||||||
database and the user email has not changed. This function also checks
|
|
||||||
if the token is being used within a certain period, depending on the
|
|
||||||
context. The default contexts supported by this function are either
|
|
||||||
"confirm", for account confirmation emails, and "reset_password",
|
|
||||||
for resetting the password. For verifying requests to change the email,
|
|
||||||
see `verify_change_email_token_query/2`.
|
|
||||||
"""
|
|
||||||
def verify_email_token_query(token, context) do
|
|
||||||
case Base.url_decode64(token, padding: false) do
|
|
||||||
{:ok, decoded_token} ->
|
|
||||||
hashed_token = :crypto.hash(@hash_algorithm, decoded_token)
|
|
||||||
days = days_for_context(context)
|
|
||||||
|
|
||||||
query =
|
|
||||||
from token in by_token_and_context_query(hashed_token, context),
|
|
||||||
join: user in assoc(token, :user),
|
|
||||||
where: token.inserted_at > ago(^days, "day") and token.sent_to == user.email,
|
|
||||||
select: user
|
|
||||||
|
|
||||||
{:ok, query}
|
|
||||||
|
|
||||||
:error ->
|
|
||||||
:error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp days_for_context("confirm"), do: @confirm_validity_in_days
|
|
||||||
defp days_for_context("reset_password"), do: @reset_password_validity_in_days
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Checks if the token is valid and returns its underlying lookup query.
|
|
||||||
|
|
||||||
The query returns the user found by the token, if any.
|
|
||||||
|
|
||||||
This is used to validate requests to change the user
|
|
||||||
email. It is different from `verify_email_token_query/2` precisely because
|
|
||||||
`verify_email_token_query/2` validates the email has not changed, which is
|
|
||||||
the starting point by this function.
|
|
||||||
|
|
||||||
The given token is valid if it matches its hashed counterpart in the
|
|
||||||
database and if it has not expired (after @change_email_validity_in_days).
|
|
||||||
The context must always start with "change:".
|
|
||||||
"""
|
|
||||||
def verify_change_email_token_query(token, "change:" <> _ = context) do
|
|
||||||
case Base.url_decode64(token, padding: false) do
|
|
||||||
{:ok, decoded_token} ->
|
|
||||||
hashed_token = :crypto.hash(@hash_algorithm, decoded_token)
|
|
||||||
|
|
||||||
query =
|
|
||||||
from token in by_token_and_context_query(hashed_token, context),
|
|
||||||
where: token.inserted_at > ago(@change_email_validity_in_days, "day")
|
|
||||||
|
|
||||||
{:ok, query}
|
|
||||||
|
|
||||||
:error ->
|
|
||||||
:error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Returns the token struct for the given token value and context.
|
|
||||||
"""
|
|
||||||
def by_token_and_context_query(token, context) do
|
|
||||||
from UserToken, where: [token: ^token, context: ^context]
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Gets all tokens for the given user for the given contexts.
|
|
||||||
"""
|
|
||||||
def by_user_and_contexts_query(user, :all) do
|
|
||||||
from t in UserToken, where: t.user_id == ^user.id
|
|
||||||
end
|
|
||||||
|
|
||||||
def by_user_and_contexts_query(user, [_ | _] = contexts) do
|
|
||||||
from t in UserToken, where: t.user_id == ^user.id and t.context in ^contexts
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
defmodule Bright.B2 do
|
||||||
|
@moduledoc """
|
||||||
|
The B2 context.
|
||||||
|
"""
|
||||||
|
import Ecto.Query, warn: false
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias ExAws.S3
|
||||||
|
|
||||||
|
alias Bright.Repo
|
||||||
|
alias Bright.Cache
|
||||||
|
|
||||||
|
alias Bright.B2
|
||||||
|
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Put a file from local disk to Backblaze
|
||||||
|
"""
|
||||||
|
def put(local_file, object_key) do
|
||||||
|
|
||||||
|
bucket = Application.get_env(:bright, :aws_bucket)
|
||||||
|
if bucket === nil do
|
||||||
|
raise("bucket specification is missing")
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
local_file
|
||||||
|
|> S3.Upload.stream_file
|
||||||
|
|> S3.upload(bucket, object_key)
|
||||||
|
|> ExAws.request
|
||||||
|
|> case do
|
||||||
|
{:ok, %{status_code: 200}} -> {:ok, object_key}
|
||||||
|
{:error, reason} -> {:error, reason}
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Download a file from Backblaze to local disk
|
||||||
|
"""
|
||||||
|
def get(object_key, local_file) do
|
||||||
|
# B2.get("test/SampleVideo_1280x720_1mb.mp4", local_file)
|
||||||
|
|
||||||
|
bucket = Application.get_env(:bright, :aws_bucket)
|
||||||
|
|
||||||
|
|
||||||
|
S3.download_file(bucket, object_key, local_file)
|
||||||
|
|> ExAws.request
|
||||||
|
|> case do
|
||||||
|
{:ok, :done} -> {:ok, local_file}
|
||||||
|
{:error, reason} -> {:error, reason}
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# defp upload_to_s3(body, key) do
|
||||||
|
# body
|
||||||
|
# |> S3.upload(@bucket_name, key)
|
||||||
|
# |> ExAws.request()
|
||||||
|
# |> case do
|
||||||
|
# {:ok, _response} -> :ok
|
||||||
|
# {:error, reason} -> {:error, reason}
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
|
||||||
|
end
|
|
@ -0,0 +1,178 @@
|
||||||
|
defmodule Bright.Cache do
|
||||||
|
@moduledoc """
|
||||||
|
A simple caching module that saves files to the `/tmp` directory.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Bright.Streams.Vod
|
||||||
|
|
||||||
|
|
||||||
|
@cache_dir "/tmp/bright_cache"
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
def generate_filename(input) do
|
||||||
|
prefix = :crypto.strong_rand_bytes(6) |> Base.encode64(padding: false) |> String.replace(~r/[^a-zA-Z0-9]/, "")
|
||||||
|
base = Path.basename(input)
|
||||||
|
"#{prefix}-#{base}"
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Ensure the cache directory exists
|
||||||
|
defp ensure_cache_dir! do
|
||||||
|
unless File.exists?(@cache_dir) do
|
||||||
|
File.mkdir_p!(@cache_dir)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Save data to the cache with a given key.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> Bright.Cache.put("example_key", "example_data")
|
||||||
|
:ok
|
||||||
|
"""
|
||||||
|
def put(key, data) do
|
||||||
|
|
||||||
|
ensure_cache_dir!()
|
||||||
|
|
||||||
|
file_path = cache_file_path(key)
|
||||||
|
|
||||||
|
case File.write(file_path, data) do
|
||||||
|
:ok ->
|
||||||
|
Logger.debug("[Cache] Saved key #{key} to #{file_path}")
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
Logger.error("[Cache] Failed to save key #{key}: #{reason}")
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Save a VOD file to the cache using its origin_temp_input_url.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> Bright.Cache.put(%Vod{key: "vod_key", origin_temp_input_url: "http://example.com/video.mp4"})
|
||||||
|
:ok
|
||||||
|
"""
|
||||||
|
def put(%Vod{origin_temp_input_url: url}) do
|
||||||
|
ensure_cache_dir!()
|
||||||
|
|
||||||
|
key = generate_filename(url)
|
||||||
|
file_path = cache_file_path(key)
|
||||||
|
|
||||||
|
case HTTPoison.get(url) do
|
||||||
|
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
|
||||||
|
case File.write(file_path, body) do
|
||||||
|
:ok ->
|
||||||
|
Logger.debug("[Cache] Downloaded and saved VOD #{key} from #{url} to #{file_path}")
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
Logger.error("[Cache] Failed to save VOD #{key}: #{reason}")
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, %HTTPoison.Response{status_code: status}} ->
|
||||||
|
Logger.error("[Cache] Failed to download VOD #{key}: HTTP #{status}")
|
||||||
|
{:error, :http_error}
|
||||||
|
|
||||||
|
{:error, %HTTPoison.Error{reason: reason}} ->
|
||||||
|
Logger.error("[Cache] Failed to download VOD #{key}: #{reason}")
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Retrieve data from the cache for a given key.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> Bright.Cache.get("example_key")
|
||||||
|
{:ok, "example_data"}
|
||||||
|
|
||||||
|
iex> Bright.Cache.get("nonexistent_key")
|
||||||
|
:error
|
||||||
|
"""
|
||||||
|
def get(key) do
|
||||||
|
ensure_cache_dir!()
|
||||||
|
|
||||||
|
file_path = cache_file_path(key)
|
||||||
|
|
||||||
|
case File.read(file_path) do
|
||||||
|
{:ok, data} ->
|
||||||
|
Logger.debug("[Cache] Retrieved key #{key} from #{file_path}")
|
||||||
|
{:ok, data}
|
||||||
|
|
||||||
|
{:error, :enoent} ->
|
||||||
|
Logger.debug("[Cache] Key #{key} not found in cache")
|
||||||
|
:error
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
Logger.error("[Cache] Failed to retrieve key #{key}: #{reason}")
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Delete a cached item by its key.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> Bright.Cache.delete("example_key")
|
||||||
|
:ok
|
||||||
|
|
||||||
|
iex> Bright.Cache.delete("nonexistent_key")
|
||||||
|
:ok
|
||||||
|
"""
|
||||||
|
def delete(key) do
|
||||||
|
ensure_cache_dir!()
|
||||||
|
|
||||||
|
file_path = cache_file_path(key)
|
||||||
|
|
||||||
|
case File.rm(file_path) do
|
||||||
|
:ok ->
|
||||||
|
Logger.debug("[Cache] Deleted key #{key} from #{file_path}")
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:error, :enoent} ->
|
||||||
|
Logger.debug("[Cache] Key #{key} not found for deletion")
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
Logger.error("[Cache] Failed to delete key #{key}: #{reason}")
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Clear all cached data.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> Bright.Cache.clear()
|
||||||
|
:ok
|
||||||
|
"""
|
||||||
|
def clear do
|
||||||
|
ensure_cache_dir!()
|
||||||
|
|
||||||
|
case File.rm_rf(@cache_dir) do
|
||||||
|
{:ok, _} ->
|
||||||
|
Logger.debug("[Cache] Cleared all cached data")
|
||||||
|
ensure_cache_dir!()
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
Logger.error("[Cache] Failed to clear cache: #{reason}")
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper function to generate the file path for a given key
|
||||||
|
defp cache_file_path(key) do
|
||||||
|
Path.join(@cache_dir, key)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,90 @@
|
||||||
|
defmodule Bright.Images do
|
||||||
|
@moduledoc """
|
||||||
|
Images context for image generation and processing
|
||||||
|
"""
|
||||||
|
|
||||||
|
import FFmpex
|
||||||
|
use FFmpex.Options
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
|
||||||
|
def get_video_duration(file_path) do
|
||||||
|
case FFprobe.streams(file_path) do
|
||||||
|
{:ok, streams} ->
|
||||||
|
streams
|
||||||
|
|> Enum.find(fn stream -> stream["codec_type"] == "video" end)
|
||||||
|
|> case do
|
||||||
|
nil -> {:error, "No video stream found"}
|
||||||
|
video_stream ->
|
||||||
|
duration =
|
||||||
|
video_stream
|
||||||
|
|> Map.get("duration", %{})
|
||||||
|
|
||||||
|
case duration do
|
||||||
|
nil -> {:error, "Duration not found or could not be calculated"}
|
||||||
|
duration -> {:ok, duration}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
{:error, reason} -> {:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_video_framecount(file_path) do
|
||||||
|
case FFprobe.streams(file_path) do
|
||||||
|
{:ok, streams} ->
|
||||||
|
streams
|
||||||
|
|> Enum.find(fn stream -> stream["codec_type"] == "video" end)
|
||||||
|
|> case do
|
||||||
|
nil -> {:error, "No video stream found"}
|
||||||
|
video_stream ->
|
||||||
|
nb_frames =
|
||||||
|
video_stream
|
||||||
|
|> Map.get("nb_frames", %{})
|
||||||
|
|
||||||
|
case nb_frames do
|
||||||
|
nil -> {:error, "nb_frames not found"}
|
||||||
|
%{} -> {:error, "nb_frames not found. (empty map)"}
|
||||||
|
nb_frames ->
|
||||||
|
case Integer.parse(nb_frames) do
|
||||||
|
{number, _} -> {:ok, number}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
{:error, reason} -> {:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def create_thumbnail(input_file, output_file) do
|
||||||
|
|
||||||
|
case get_video_framecount(input_file) do
|
||||||
|
{:error, reason} -> {:error, reason}
|
||||||
|
{:ok, framecount} ->
|
||||||
|
|
||||||
|
frame_interval = div(framecount, 25)
|
||||||
|
scale_width = 160
|
||||||
|
tile_grid = "5x5"
|
||||||
|
|
||||||
|
# ffmpeg -y -i ~/Videos/moose-encounter_75.mp4 -frames:v 1 -vf 'select=not(mod(n\,257)),scale=160:-1,tile=5x5' -update 1 -fps_mode passthrough ~/Videos/thumb.jpg
|
||||||
|
command =
|
||||||
|
FFmpex.new_command
|
||||||
|
|> add_global_option(option_y())
|
||||||
|
|> add_input_file(input_file)
|
||||||
|
|> add_output_file(output_file)
|
||||||
|
|> add_file_option(option_vframes(1))
|
||||||
|
|> add_file_option(option_vf("select=not(mod(n\\,#{frame_interval})),scale=#{scale_width}:-1,tile=#{tile_grid}"))
|
||||||
|
|> add_file_option(option_vsync(1)) # -vsync is deprecated in ffmpeg but ffmpex doesn't have the modern replacement, -fps_mode
|
||||||
|
# |> add_file_option(option_update(1)) # ffmpeg complains but it doesn't necessarily need this. I'm omitting because ffmpex doesn't know this function
|
||||||
|
# |> add_file_option(option_fps_mode("passthrough")) # -fps_mode is the modern replacement for -vsync
|
||||||
|
|
||||||
|
execute(command)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
end
|
|
@ -1,138 +0,0 @@
|
||||||
defmodule Bright.Jobs.CreateHlsPlaylist do
|
|
||||||
|
|
||||||
alias Bright.Repo
|
|
||||||
alias Bright.Streams.Vod
|
|
||||||
|
|
||||||
use Oban.Worker,
|
|
||||||
queue: :default,
|
|
||||||
max_attempts: 3,
|
|
||||||
tags: ["video", "vod"]
|
|
||||||
|
|
||||||
require Logger
|
|
||||||
|
|
||||||
@auth_token System.get_env("SUPERSTREAMER_AUTH_TOKEN")
|
|
||||||
@api_url System.get_env("SUPERSTREAMER_API_URL")
|
|
||||||
@public_s3_endpoint System.get_env("PUBLIC_S3_ENDPOINT")
|
|
||||||
|
|
||||||
@impl Oban.Worker
|
|
||||||
def perform(%Oban.Job{args: %{"vod_id" => vod_id, "input_url" => input_url}}) do
|
|
||||||
Logger.info("Starting CreateHlsPlaylist job",
|
|
||||||
job_id: job.id,
|
|
||||||
vod_id: vod_id,
|
|
||||||
input_url: input_url
|
|
||||||
)
|
|
||||||
|
|
||||||
with {:ok, transcode_job_id} <- start_transcode(input_url),
|
|
||||||
{:ok, asset_id} <- wait_for_job(transcode_job_id),
|
|
||||||
{:ok, package_job_id} <- start_package(asset_id),
|
|
||||||
{:ok, _} <- wait_for_job(package_job_id) do
|
|
||||||
update_vod_playlist_url(vod_id, package_job_id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp start_transcode(input_url) do
|
|
||||||
payload = %{
|
|
||||||
inputs: [
|
|
||||||
%{type: "video", path: input_url},
|
|
||||||
%{type: "audio", path: input_url, language: "eng"}
|
|
||||||
],
|
|
||||||
streams: [
|
|
||||||
%{type: "video", codec: "h264", height: 360},
|
|
||||||
%{type: "video", codec: "h264", height: 144},
|
|
||||||
%{type: "audio", codec: "aac"}
|
|
||||||
],
|
|
||||||
packageAfter: false
|
|
||||||
}
|
|
||||||
|
|
||||||
case HTTPoison.post("#{@api_url}/transcode", Jason.encode!(payload), headers()) do
|
|
||||||
{:ok, %{status_code: 200, body: body}} ->
|
|
||||||
%{"jobId" => job_id} = Jason.decode!(body)
|
|
||||||
{:ok, job_id}
|
|
||||||
|
|
||||||
error ->
|
|
||||||
Logger.error("Failed to start transcode: #{inspect(error)}")
|
|
||||||
{:error, :transcode_failed}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp start_package(asset_id) do
|
|
||||||
payload = %{
|
|
||||||
assetId: asset_id,
|
|
||||||
concurrency: 5,
|
|
||||||
public: false,
|
|
||||||
name: "vod_#{asset_id}"
|
|
||||||
}
|
|
||||||
|
|
||||||
case HTTPoison.post("#{@api_url}/package", Jason.encode!(payload), headers()) do
|
|
||||||
{:ok, %{status_code: 200, body: body}} ->
|
|
||||||
%{"jobId" => job_id} = Jason.decode!(body)
|
|
||||||
{:ok, job_id}
|
|
||||||
|
|
||||||
error ->
|
|
||||||
Logger.error("Failed to start package: #{inspect(error)}")
|
|
||||||
{:error, :package_failed}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp wait_for_job(job_id) do
|
|
||||||
case poll_job_status(job_id) do
|
|
||||||
{:ok, %{"state" => "completed", "outputData" => output_data}} ->
|
|
||||||
case Jason.decode(output_data) do
|
|
||||||
{:ok, %{"assetId" => asset_id}} -> {:ok, asset_id}
|
|
||||||
_ -> {:ok, job_id} # For package jobs, we just need the job_id
|
|
||||||
end
|
|
||||||
|
|
||||||
{:ok, %{"state" => "failed", "stacktrace" => stacktrace}} ->
|
|
||||||
Logger.error("Job failed: #{job_id}, stacktrace: #{inspect(stacktrace)}")
|
|
||||||
{:error, :job_failed}
|
|
||||||
|
|
||||||
error ->
|
|
||||||
Logger.error("Error polling job status: #{inspect(error)}")
|
|
||||||
{:error, :polling_failed}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp poll_job_status(job_id, attempts \\ 0) do
|
|
||||||
if attempts >= 360 do # 30 minutes maximum (5 seconds * 360)
|
|
||||||
{:error, :timeout}
|
|
||||||
else
|
|
||||||
case HTTPoison.get("#{@api_url}/jobs/#{job_id}", headers()) do
|
|
||||||
{:ok, %{status_code: 200, body: body}} ->
|
|
||||||
job = Jason.decode!(body)
|
|
||||||
|
|
||||||
case job do
|
|
||||||
%{"state" => state} when state in ["completed", "failed"] ->
|
|
||||||
{:ok, job}
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
Process.sleep(5000) # Wait 5 seconds before next poll
|
|
||||||
poll_job_status(job_id, attempts + 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
error ->
|
|
||||||
Logger.error("Failed to poll job status: #{inspect(error)}")
|
|
||||||
{:error, :polling_failed}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp update_vod_playlist_url(vod_id, package_job_id) do
|
|
||||||
playlist_url = generate_playlist_url(package_job_id)
|
|
||||||
|
|
||||||
case Vod.update_vod(vod_id, %{playlist_url: playlist_url}) do
|
|
||||||
{:ok, _vod} -> :ok
|
|
||||||
error ->
|
|
||||||
Logger.error("Failed to update VOD playlist URL: #{inspect(error)}")
|
|
||||||
{:error, :update_failed}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp generate_playlist_url(job_id), do: "#{@public_s3_endpoint}/package/#{job_id}/hls/master.m3u8"
|
|
||||||
|
|
||||||
defp headers do
|
|
||||||
[
|
|
||||||
{"Authorization", "Bearer #{@auth_token}"},
|
|
||||||
{"Content-Type", "application/json"}
|
|
||||||
]
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,31 +0,0 @@
|
||||||
defmodule Bright.Jobs.ProcessVod do
|
|
||||||
use Oban.Worker, queue: :default, max_attempts: 3
|
|
||||||
|
|
||||||
require Logger
|
|
||||||
|
|
||||||
alias Bright.Repo
|
|
||||||
alias Bright.Streams.Vod
|
|
||||||
alias Bright.Jobs.CreateHlsPlaylist
|
|
||||||
|
|
||||||
@impl Oban.Worker
|
|
||||||
def perform(%Oban.Job{args: %{"id" => id}} = job) do
|
|
||||||
Logger.info("Performing job: #{inspect(job)}")
|
|
||||||
|
|
||||||
|
|
||||||
vod = Repo.get!(Vod, id)
|
|
||||||
|
|
||||||
cond do
|
|
||||||
vod.playlist_url == nil and vod.origin_temp_input_url != nil ->
|
|
||||||
queue_create_hls_playlist(vod)
|
|
||||||
:ok
|
|
||||||
|
|
||||||
true ->
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp queue_create_hls_playlist(%Vod{id: id, origin_temp_input_url: url}) do
|
|
||||||
job_args = %{vod_id: id, input_url: url}
|
|
||||||
Oban.insert!(CreateHlsPlaylist.new(job_args))
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,7 +1,7 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
defmodule Bright.Jobs.CreateHlsPlaylist do
|
defmodule Bright.ObanWorkers.CreateHlsPlaylist do
|
||||||
use Oban.Worker, queue: :default, max_attempts: 1
|
use Oban.Worker, queue: :default, max_attempts: 1
|
||||||
|
|
||||||
alias Bright.Repo
|
alias Bright.Repo
|
||||||
|
@ -9,12 +9,16 @@ defmodule Bright.Jobs.CreateHlsPlaylist do
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
@auth_token System.get_env("SUPERSTREAMER_AUTH_TOKEN")
|
@auth_token Application.get_env(:bright, :superstreamer_auth_token)
|
||||||
@api_url System.get_env("SUPERSTREAMER_URL")
|
@superstreamer_url Application.get_env(:bright, :superstreamer_url)
|
||||||
@public_s3_endpoint System.get_env("PUBLIC_S3_ENDPOINT")
|
@public_s3_endpoint Application.get_env(:bright, :s3_cdn_endpoint)
|
||||||
|
|
||||||
|
|
||||||
|
# args: %{"vod_id" => 10, "input_url" => "http://38.242.193.246:8081/fixtures/2024-12-19T03-10-30Z.ts"}
|
||||||
|
|
||||||
@impl Oban.Worker
|
@impl Oban.Worker
|
||||||
def perform(%Oban.Job{args: %{"vod_id" => vod_id, "input_url" => input_url}}) do
|
def perform(%Oban.Job{args: %{"vod_id" => vod_id, "input_url" => input_url}}) do
|
||||||
|
Logger.info(">>>> create_hls_playlist is performing. input_url=#{input_url} vod_id=#{vod_id}")
|
||||||
vod = Repo.get!(Vod, vod_id)
|
vod = Repo.get!(Vod, vod_id)
|
||||||
|
|
||||||
payload = build_payload(input_url)
|
payload = build_payload(input_url)
|
||||||
|
@ -39,12 +43,12 @@ defmodule Bright.Jobs.CreateHlsPlaylist do
|
||||||
%{
|
%{
|
||||||
"inputs" => [
|
"inputs" => [
|
||||||
%{"type" => "audio", "path" => input_url, "language" => "eng"},
|
%{"type" => "audio", "path" => input_url, "language" => "eng"},
|
||||||
%{"type" => "video", "path" => input_url}
|
# %{"type" => "video", "path" => input_url}
|
||||||
],
|
],
|
||||||
"streams" => [
|
"streams" => [
|
||||||
%{"type" => "video", "codec" => "h264", "height" => 1080},
|
# %{"type" => "video", "codec" => "h264", "height" => 1080},
|
||||||
%{"type" => "video", "codec" => "h264", "height" => 720},
|
# %{"type" => "video", "codec" => "h264", "height" => 720},
|
||||||
%{"type" => "video", "codec" => "h264", "height" => 144},
|
# %{"type" => "video", "codec" => "h264", "height" => 144},
|
||||||
%{"type" => "audio", "codec" => "aac"}
|
%{"type" => "audio", "codec" => "aac"}
|
||||||
],
|
],
|
||||||
"tag" => "create_hls_playlist"
|
"tag" => "create_hls_playlist"
|
||||||
|
@ -54,14 +58,19 @@ defmodule Bright.Jobs.CreateHlsPlaylist do
|
||||||
|
|
||||||
defp start_transcode(payload) do
|
defp start_transcode(payload) do
|
||||||
Logger.info("Starting transcode with payload: #{inspect(payload)}")
|
Logger.info("Starting transcode with payload: #{inspect(payload)}")
|
||||||
|
IO.puts "Starting transcode with payload: #{inspect(payload)}"
|
||||||
|
|
||||||
headers = auth_headers()
|
headers = auth_headers()
|
||||||
|
|
||||||
Logger.info("auth headers as follows")
|
Logger.info("auth headers as follows")
|
||||||
Logger.info(inspect(headers))
|
Logger.info(inspect(headers))
|
||||||
|
|
||||||
Logger.info("now we will POST /transcode to api_url=#{@api_url}")
|
if is_nil(@superstreamer_url) do
|
||||||
data = case HTTPoison.post("#{@api_url}/transcode", Jason.encode!(payload), headers) do
|
Logger.error("The @superstreamer_url is nil. This must be set before proceeding.")
|
||||||
|
raise "The @superstreamer_url is not configured."
|
||||||
|
end
|
||||||
|
Logger.info("now we will POST /transcode to superstreamer_url=#{@superstreamer_url}")
|
||||||
|
data = case HTTPoison.post("#{@superstreamer_url}/transcode", Jason.encode!(payload), headers) do
|
||||||
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
|
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
|
||||||
{:ok, Jason.decode!(body)}
|
{:ok, Jason.decode!(body)}
|
||||||
|
|
||||||
|
@ -71,6 +80,9 @@ defmodule Bright.Jobs.CreateHlsPlaylist do
|
||||||
{:error, %HTTPoison.Error{reason: reason}} ->
|
{:error, %HTTPoison.Error{reason: reason}} ->
|
||||||
{:error, reason}
|
{:error, reason}
|
||||||
|
|
||||||
|
[] ->
|
||||||
|
{:error, "We got an empty response from Superstreamer"}
|
||||||
|
|
||||||
failed ->
|
failed ->
|
||||||
Logger.error("Failed to POST /transcode: #{inspect(failed)}")
|
Logger.error("Failed to POST /transcode: #{inspect(failed)}")
|
||||||
{:error, :failed}
|
{:error, :failed}
|
||||||
|
@ -104,8 +116,8 @@ defmodule Bright.Jobs.CreateHlsPlaylist do
|
||||||
Logger.info("auth headers as follows")
|
Logger.info("auth headers as follows")
|
||||||
Logger.info(inspect(headers))
|
Logger.info(inspect(headers))
|
||||||
|
|
||||||
Logger.info("now we will POST /package to api_url=#{@api_url}")
|
Logger.info("now we will POST /package to superstreamer_url=#{@superstreamer_url}")
|
||||||
data = case HTTPoison.post("#{@api_url}/package", Jason.encode!(payload), headers) do
|
data = case HTTPoison.post("#{@superstreamer_url}/package", Jason.encode!(payload), headers) do
|
||||||
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
|
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
|
||||||
{:ok, Jason.decode!(body)}
|
{:ok, Jason.decode!(body)}
|
||||||
|
|
||||||
|
@ -161,14 +173,14 @@ defmodule Bright.Jobs.CreateHlsPlaylist do
|
||||||
Logger.info(">>>> formatted=#{inspect(formatted)}")
|
Logger.info(">>>> formatted=#{inspect(formatted)}")
|
||||||
{:halt, {:ok, formatted["assetId"]}}
|
{:halt, {:ok, formatted["assetId"]}}
|
||||||
|
|
||||||
|
{:ok, "failed", _data} ->
|
||||||
|
{:halt, {:error, "superstreamer reports that the job failed."}}
|
||||||
|
|
||||||
{:ok, state, data} ->
|
{:ok, state, data} ->
|
||||||
Logger.info("Job ID #{job_id} #{state}. Re-polling in #{poll_interval}.")
|
Logger.info("Job ID #{job_id} #{state}. Re-polling in #{poll_interval}.")
|
||||||
:timer.sleep(poll_interval)
|
:timer.sleep(poll_interval)
|
||||||
{:cont, acc}
|
{:cont, acc}
|
||||||
|
|
||||||
{:ok, "failed", _data} ->
|
|
||||||
{:halt, {:error, "superstreamer reports that the job failed."}}
|
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
Logger.error("Error polling job ID #{job_id}: #{inspect(reason)}")
|
Logger.error("Error polling job ID #{job_id}: #{inspect(reason)}")
|
||||||
{:halt, {:error, reason}}
|
{:halt, {:error, reason}}
|
||||||
|
@ -179,7 +191,7 @@ defmodule Bright.Jobs.CreateHlsPlaylist do
|
||||||
defp get_job_status(job_id) do
|
defp get_job_status(job_id) do
|
||||||
headers = auth_headers()
|
headers = auth_headers()
|
||||||
|
|
||||||
data = case HTTPoison.get("#{@api_url}/jobs/#{job_id}", headers) do
|
data = case HTTPoison.get("#{@superstreamer_url}/jobs/#{job_id}", headers) do
|
||||||
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
|
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
|
||||||
{:ok, Jason.decode!(body)}
|
{:ok, Jason.decode!(body)}
|
||||||
|
|
||||||
|
@ -208,7 +220,7 @@ defmodule Bright.Jobs.CreateHlsPlaylist do
|
||||||
|
|
||||||
defp update_vod_with_playlist_url(vod, asset_id) do
|
defp update_vod_with_playlist_url(vod, asset_id) do
|
||||||
playlist_url = generate_playlist_url(asset_id)
|
playlist_url = generate_playlist_url(asset_id)
|
||||||
|
Logger.info("playlist_url=#{playlist_url}")
|
||||||
vod
|
vod
|
||||||
|> Ecto.Changeset.change(playlist_url: playlist_url)
|
|> Ecto.Changeset.change(playlist_url: playlist_url)
|
||||||
|> Repo.update!()
|
|> Repo.update!()
|
|
@ -0,0 +1,47 @@
|
||||||
|
defmodule Bright.ObanWorkers.CreateS3Asset do
|
||||||
|
use Oban.Worker, queue: :default, max_attempts: 3
|
||||||
|
|
||||||
|
alias ExAws.S3
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@bucket_name System.get_env("AWS_BUCKET")
|
||||||
|
|
||||||
|
@impl Oban.Worker
|
||||||
|
def perform(%Oban.Job{args: %{"input_url" => input_url, "vod_id" => vod_id}}) do
|
||||||
|
Logger.info("@todo implementation needed")
|
||||||
|
# random_string = for _ <- 1..12, into: "", do: <<Enum.random(~c"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")>>
|
||||||
|
# output_file = "/tmp/#{random_string}-#{key}"
|
||||||
|
# with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- fetch_file(input_url),
|
||||||
|
# {:ok, local_path} <- save_file(body, key),
|
||||||
|
# :ok <- upload_to_s3(local_path, key) do
|
||||||
|
# :ok
|
||||||
|
# else
|
||||||
|
# {:error, reason} ->
|
||||||
|
# Logger.error("Failed to process job: #{inspect(reason)}")
|
||||||
|
# {:error, reason}
|
||||||
|
# end
|
||||||
|
end
|
||||||
|
|
||||||
|
# defp fetch_file(url) do
|
||||||
|
# case HTTPoison.get(url, [], stream_to: self()) do
|
||||||
|
# {:ok, response} when response.status_code in 200..299 ->
|
||||||
|
# {:ok, response}
|
||||||
|
|
||||||
|
# {:ok, %HTTPoison.Response{status_code: status}} ->
|
||||||
|
# {:error, "HTTP request failed with status: #{status}"}
|
||||||
|
|
||||||
|
# {:error, %HTTPoison.Error{reason: reason}} ->
|
||||||
|
# {:error, reason}
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
|
||||||
|
# defp upload_to_s3(body, key) do
|
||||||
|
# body
|
||||||
|
# |> S3.upload(@bucket_name, key)
|
||||||
|
# |> ExAws.request()
|
||||||
|
# |> case do
|
||||||
|
# {:ok, _response} -> :ok
|
||||||
|
# {:error, reason} -> {:error, reason}
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
end
|
|
@ -0,0 +1,46 @@
|
||||||
|
defmodule Bright.ObanWorkers.CreateThumbnail do
|
||||||
|
use Oban.Worker, queue: :default, max_attempts: 3
|
||||||
|
|
||||||
|
alias Bright.Cache
|
||||||
|
alias Bright.Images
|
||||||
|
alias Bright.B2
|
||||||
|
alias Bright.Streams.Vod
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@impl Oban.Worker
|
||||||
|
def perform(%Oban.Job{args: %{"vod_id" => vod_id, "origin_temp_input_url" => origin_temp_input_url}}) do
|
||||||
|
|
||||||
|
IO.puts ">>>> CreateThumbnail is performing. with vod_id=#{vod_id}"
|
||||||
|
vod = Repo.get!(Vod, vod_id)
|
||||||
|
|
||||||
|
with {:ok, cache_filename} <- Cache.put(origin_temp_input_url),
|
||||||
|
{:ok, thumb_filename} <- Images.create_thumbnail(cache_filename),
|
||||||
|
{:ok, s3Asset} <- B2.put(thumb_filename) do
|
||||||
|
update_vod_with_thumbnail_url(vod, s3Asset.cdn_url)
|
||||||
|
else
|
||||||
|
{:error, reason} ->
|
||||||
|
Logger.error("Failed to create HLS playlist for VOD ID #{vod_id}: #{inspect(reason)}")
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# vod
|
||||||
|
# |> Cache.put
|
||||||
|
# |> Images.create_thumbnail
|
||||||
|
# |> B2.put
|
||||||
|
|
||||||
|
defp generate_thumbnail_url(basename), do: "#{@public_s3_endpoint}/#{basename}"
|
||||||
|
|
||||||
|
defp update_vod_with_thumbnail_url(vod, thumbnail_url) do
|
||||||
|
basename = Path.basename(thumbnail_url)
|
||||||
|
thumbnail_url = generate_thumbnail_url(basename)
|
||||||
|
Logger.info("thumbnail_url=#{thumbnail_url}")
|
||||||
|
vod
|
||||||
|
|> Ecto.Changeset.change(thumbnail_url: thumbnail_url)
|
||||||
|
|> Repo.update!()
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,55 @@
|
||||||
|
defmodule Bright.ObanWorkers.ProcessVod do
|
||||||
|
use Oban.Worker, queue: :default, max_attempts: 3
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias Bright.Repo
|
||||||
|
alias Bright.Streams.Vod
|
||||||
|
alias Bright.ObanWorkers.{
|
||||||
|
CreateHlsPlaylist,
|
||||||
|
CreateS3Asset,
|
||||||
|
CreateThumbnail
|
||||||
|
}
|
||||||
|
|
||||||
|
@impl Oban.Worker
|
||||||
|
def perform(%Oban.Job{args: %{"id" => id}} = job) do
|
||||||
|
Logger.info("Performing job: #{inspect(job)}")
|
||||||
|
|
||||||
|
|
||||||
|
vod = Repo.get!(Vod, id)
|
||||||
|
|
||||||
|
if vod.playlist_url == nil and vod.origin_temp_input_url != nil do
|
||||||
|
queue_create_hls_playlist(vod)
|
||||||
|
end
|
||||||
|
|
||||||
|
if vod.s3_key == nil do
|
||||||
|
queue_create_s3_asset(vod)
|
||||||
|
end
|
||||||
|
|
||||||
|
if vod.thumbnail_url == nil and vod.origin_temp_input_url != nil do
|
||||||
|
queue_create_thumbnail(vod)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
:ok
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
defp queue_create_hls_playlist(%Vod{id: id, origin_temp_input_url: url}) do
|
||||||
|
job_args = %{vod_id: id, input_url: url}
|
||||||
|
Oban.insert!(CreateHlsPlaylist.new(job_args))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp queue_create_s3_asset(%Vod{id: id, origin_temp_input_url: url}) do
|
||||||
|
job_args = %{vod_id: id, input_url: url}
|
||||||
|
Oban.insert!(CreateS3Asset.new(job_args))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp queue_create_thumbnail(%Vod{id: id, origin_temp_input_url: url}) do
|
||||||
|
job_args = %{vod_id: id}
|
||||||
|
Oban.insert!(CreateThumbnail.new(job_args))
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
end
|
|
@ -0,0 +1,27 @@
|
||||||
|
defmodule Bright.Regexp do
|
||||||
|
def cache_buster, do: ~r/\?v=.*\z/
|
||||||
|
|
||||||
|
def email, do: ~r/^[^@ ]+@[^ ]+\.[^ ]+$/
|
||||||
|
|
||||||
|
def email_message, do: "must be a valid email address"
|
||||||
|
|
||||||
|
def http, do: ~r/^https?:\/\//
|
||||||
|
|
||||||
|
def http_message, do: "must include http(s)://"
|
||||||
|
|
||||||
|
def tag, do: ~r/(?<open><\/?)(?<tag>.*?)(?<close>>)/
|
||||||
|
|
||||||
|
def tag(name), do: ~r/(?<open><\/?)#{name}(?<close>>)/
|
||||||
|
|
||||||
|
def social, do: ~r/\A[a-z|A-Z|0-9|_|-]+\z/
|
||||||
|
|
||||||
|
def social_message, do: "just the username, plz"
|
||||||
|
|
||||||
|
def slug, do: ~r/\A[a-z|0-9|_|-]+\z/
|
||||||
|
|
||||||
|
def slug_message, do: "valid chars: a-z, 0-9, -, _"
|
||||||
|
|
||||||
|
def name, do: ~r/\A[a-zA-Z0-9_\- ]+\z/
|
||||||
|
|
||||||
|
def name_message, do: "valid chars: a-z, A-Z, 0-9, -, _, (space)"
|
||||||
|
end
|
|
@ -3,6 +3,7 @@ defmodule Bright.Streams do
|
||||||
The Streams context.
|
The Streams context.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
require Logger
|
||||||
import Ecto.Query, warn: false
|
import Ecto.Query, warn: false
|
||||||
alias Bright.Repo
|
alias Bright.Repo
|
||||||
|
|
||||||
|
@ -208,10 +209,14 @@ defmodule Bright.Streams do
|
||||||
|
|
||||||
|
|
||||||
defp maybe_enqueue_process_vod(%Vod{id: id, origin_temp_input_url: origin_temp_input_url} = vod) do
|
defp maybe_enqueue_process_vod(%Vod{id: id, origin_temp_input_url: origin_temp_input_url} = vod) do
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if origin_temp_input_url do
|
if origin_temp_input_url do
|
||||||
|
|
||||||
|
|
||||||
%{id: id, origin_temp_input_url: origin_temp_input_url}
|
%{id: id, origin_temp_input_url: origin_temp_input_url}
|
||||||
|> Bright.Jobs.ProcessVod.new()
|
|> Bright.ObanWorkers.ProcessVod.new()
|
||||||
|> Oban.insert()
|
|> Oban.insert()
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,6 +14,7 @@ defmodule Bright.Streams.Vod do
|
||||||
field :ipfs_cid, :string
|
field :ipfs_cid, :string
|
||||||
field :torrent, :string
|
field :torrent, :string
|
||||||
field :notes, :string
|
field :notes, :string
|
||||||
|
field :thumbnail_url, :string
|
||||||
|
|
||||||
belongs_to :stream, Bright.Streams.Stream
|
belongs_to :stream, Bright.Streams.Stream
|
||||||
|
|
||||||
|
@ -23,7 +24,7 @@ defmodule Bright.Streams.Vod do
|
||||||
@doc false
|
@doc false
|
||||||
def changeset(vod, attrs) do
|
def changeset(vod, attrs) do
|
||||||
vod
|
vod
|
||||||
|> cast(attrs, [:s3_cdn_url, :s3_upload_id, :s3_key, :s3_bucket, :mux_asset_id, :mux_playback_id, :ipfs_cid, :torrent, :stream_id, :origin_temp_input_url, :playlist_url])
|
|> cast(attrs, [:s3_cdn_url, :s3_upload_id, :s3_key, :s3_bucket, :mux_asset_id, :mux_playback_id, :ipfs_cid, :torrent, :stream_id, :origin_temp_input_url, :playlist_url, :thumbnail_url])
|
||||||
|> validate_required([:stream_id])
|
|> validate_required([:stream_id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
defmodule Bright.Uploads do
|
||||||
|
@moduledoc """
|
||||||
|
Provides functions to download a file from a URL and upload a file to S3.
|
||||||
|
"""
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
alias ExAws.S3
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Downloads a file from the given URL and saves it to the specified local path.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
- `url` (string): The URL of the file to download.
|
||||||
|
- `destination_path` (string): The local path where the file will be saved.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> Bright.Uploads.download_file("https://example.com/file.txt", "./file.txt")
|
||||||
|
:ok
|
||||||
|
|
||||||
|
"""
|
||||||
|
def download_file(url, destination_path) do
|
||||||
|
case HTTPoison.get(url) do
|
||||||
|
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
|
||||||
|
IO.puts "WE GOT A GOOD RESPONSE SO LETS WRITE"
|
||||||
|
case File.write(destination_path, body) do
|
||||||
|
:ok -> {:ok, "File downloaded successfully"}
|
||||||
|
{:error, reason} -> {:error, reason}
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, %HTTPoison.Response{status_code: status_code}} ->
|
||||||
|
{:error, "Failed to download file. Status code: #{status_code}"}
|
||||||
|
|
||||||
|
{:error, :timeout} ->
|
||||||
|
{:error, "Timeout!?"}
|
||||||
|
|
||||||
|
{:error, %HTTPoison.Error{reason: reason}} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Uploads a file to an S3 bucket.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
- `file_path` (string): The local path of the file to upload.
|
||||||
|
- `bucket` (string): The name of the S3 bucket.
|
||||||
|
- `key` (string): The key under which the file will be stored in S3.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> Bright.Uploads.upload_file_to_s3("./file.txt", "my-bucket", "uploads/file.txt")
|
||||||
|
:ok
|
||||||
|
|
||||||
|
"""
|
||||||
|
def upload_file_to_s3(file_path, bucket, key) do
|
||||||
|
# IO.puts "upload_file_to_s3 with \nfile_path=#{file_path} \naws_ex region=#{Application.get_env(:bright, :aws_region)} \nbucket=#{bucket} \nkey=#{key} \naws_access_key_id=#{Application.get_env(:bright, :aws_access_key_id)} aws_secret_access_key=#{Application.get_env(:bright, :aws_secret_access_key)}"
|
||||||
|
# IO.puts "#{inspect(Application.get_all_env(:ex_aws))}"
|
||||||
|
|
||||||
|
# Throw if any S3 env vars are missing
|
||||||
|
# Application.get_env(:bright, :aws_bucket) || raise("aws_bucket is missing.")
|
||||||
|
# Application.get_env(:bright, :aws_host) || raise("aws_host is missing.")
|
||||||
|
# Application.get_env(:bright, :aws_access_key_id) || raise("aws_access_key_id is missing.")
|
||||||
|
# Application.get_env(:bright, :aws_secret_access_key) || raise("aws_secret_access_key is missing.")
|
||||||
|
# Application.get_env(:bright, :aws_region) || raise("aws_region is missing.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
case File.read(file_path) do
|
||||||
|
{:ok, file_content} ->
|
||||||
|
S3.put_object(bucket, key, file_content)
|
||||||
|
|> ExAws.request()
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -1,11 +1,10 @@
|
||||||
defmodule Bright.User do
|
defmodule Bright.User do
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
alias Bright.Repo
|
alias Bright.{Repo, Regexp}
|
||||||
|
|
||||||
schema "users" do
|
schema "users" do
|
||||||
field :name, :string
|
field :name, :string
|
||||||
field :email, :string
|
|
||||||
field :is_admin, :boolean
|
field :is_admin, :boolean
|
||||||
field :auth_token, :string
|
field :auth_token, :string
|
||||||
field :auth_token_expires_at, :utc_datetime
|
field :auth_token_expires_at, :utc_datetime
|
||||||
|
@ -17,11 +16,79 @@ defmodule Bright.User do
|
||||||
timestamps(type: :utc_datetime)
|
timestamps(type: :utc_datetime)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
def changeset(user, attrs) do
|
def changeset(user, attrs) do
|
||||||
user
|
user
|
||||||
|> cast(attrs, [:name, :email, :bio, :is_admin])
|
|> cast(attrs, [:name, :patreon_handle, :github_handle, :is_admin])
|
||||||
|> validate_required([:name, :email, :bio])
|
|> validate_required([:name, :patreon_handle])
|
||||||
|
end
|
||||||
|
|
||||||
|
defp changeset_with_allowed_attrs(user, attrs, allowed) do
|
||||||
|
user
|
||||||
|
|> cast(attrs, allowed)
|
||||||
|
|> validate_required([:name])
|
||||||
|
|> validate_format(:name, Regexp.name(), message: Regexp.name_message())
|
||||||
|
|> validate_length(:name, max: 40, message: "max 40 chars")
|
||||||
|
|> validate_format(:github_handle, Regexp.social(), message: Regexp.social_message())
|
||||||
|
|> validate_format(:patreon_handle, Regexp.social(), message: Regexp.social_message())
|
||||||
|
|> unique_constraint(:github_handle)
|
||||||
|
|> unique_constraint(:patreon_handle)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def auth_changeset(user, attrs \\ %{}),
|
||||||
|
do: cast(user, attrs, ~w(auth_token auth_token_expires_at)a)
|
||||||
|
|
||||||
|
def update_changeset(user, attrs \\ %{}) do
|
||||||
|
user
|
||||||
|
|> insert_changeset(attrs)
|
||||||
|
end
|
||||||
|
|
||||||
|
def refresh_auth_token(user, expires_in \\ 60 * 24) do
|
||||||
|
auth_token = Base.encode16(:crypto.strong_rand_bytes(8))
|
||||||
|
expires_at = Timex.add(Timex.now(), Timex.Duration.from_minutes(expires_in))
|
||||||
|
|
||||||
|
changeset =
|
||||||
|
auth_changeset(user, %{auth_token: auth_token, auth_token_expires_at: expires_at})
|
||||||
|
|
||||||
|
{:ok, user} = Repo.update(changeset)
|
||||||
|
user
|
||||||
|
end
|
||||||
|
|
||||||
|
def insert_changeset(user, attrs \\ %{}) do
|
||||||
|
allowed = ~w(name github_handle patreon_handle)a
|
||||||
|
changeset_with_allowed_attrs(user, attrs, allowed)
|
||||||
|
end
|
||||||
|
|
||||||
|
# def join(conn = %{method: "POST"}, params = %{"user" => user_params}) do
|
||||||
|
# changeset = User.insert_changeset(%User{}, user_params)
|
||||||
|
|
||||||
|
# case Repo.insert(changeset) do
|
||||||
|
# {:ok, user} ->
|
||||||
|
# welcome_community(conn, user)
|
||||||
|
|
||||||
|
# {:error, changeset} ->
|
||||||
|
# conn
|
||||||
|
# |> put_flash(:error, "Something went wrong. 😭")
|
||||||
|
# |> render(:join, changeset: changeset, user: nil)
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
|
||||||
|
|
||||||
|
# def create_from_ueberauth(%{provider: :github, info: %{nickname: handle}}) do
|
||||||
|
# changeset = User.insert_changeset(%User{}, %{github_handle: handle, patreon_handle: nil, name: handle})
|
||||||
|
|
||||||
|
# case Repo.insert(changeset) do
|
||||||
|
# {:ok, user} -> {:ok, user}
|
||||||
|
# {:error, changeset} -> {:error, changeset}
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
|
||||||
|
def get!(id) do
|
||||||
|
User
|
||||||
|
|> Repo.get(id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
@ -29,8 +96,8 @@ defmodule Bright.User do
|
||||||
Repo.get_by(__MODULE__, github_handle: handle)
|
Repo.get_by(__MODULE__, github_handle: handle)
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_by_ueberauth(%{provider: :patreon, info: %{nickname: handle}}) do
|
def get_by_ueberauth(%{provider: :patreon, info: %{id: patreon_id}}) do
|
||||||
Repo.get_by(__MODULE__, patreon_handle: handle)
|
Repo.get_by(__MODULE__, patreon_handle: patreon_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_by_ueberauth(_), do: nil
|
def get_by_ueberauth(_), do: nil
|
||||||
|
@ -45,4 +112,34 @@ defmodule Bright.User do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp now_in_seconds, do: Timex.now() |> DateTime.truncate(:second)
|
defp now_in_seconds, do: Timex.now() |> DateTime.truncate(:second)
|
||||||
|
|
||||||
|
|
||||||
|
def vod_count(user) do
|
||||||
|
user
|
||||||
|
|> Vod.authored_by()
|
||||||
|
|> Vod.published()
|
||||||
|
|> Repo.count()
|
||||||
|
end
|
||||||
|
|
||||||
|
def tag_count(user) do
|
||||||
|
user
|
||||||
|
|> Tag.authored_by()
|
||||||
|
|> Tag.published()
|
||||||
|
|> Repo.count()
|
||||||
|
end
|
||||||
|
|
||||||
|
def timestamp_count(user) do
|
||||||
|
user
|
||||||
|
|> Timestamp.authored_by()
|
||||||
|
|> Timestamp.published()
|
||||||
|
|> Repo.count()
|
||||||
|
end
|
||||||
|
|
||||||
|
def stream_count(user) do
|
||||||
|
user
|
||||||
|
|> Stream.authored_by()
|
||||||
|
|> Stream.published()
|
||||||
|
|> Repo.count()
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,7 +29,7 @@ defmodule Bright.UserFromAuth do
|
||||||
|
|
||||||
# default case if nothing matches
|
# default case if nothing matches
|
||||||
defp avatar_from_auth(auth) do
|
defp avatar_from_auth(auth) do
|
||||||
Logger.warn("#{auth.provider} needs to find an avatar URL!")
|
Logger.warning("#{auth.provider} needs to find an avatar URL!")
|
||||||
Logger.debug(Jason.encode!(auth))
|
Logger.debug(Jason.encode!(auth))
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
|
@ -102,4 +102,9 @@ defmodule Bright.Vtubers do
|
||||||
Vtuber.changeset(vtuber, attrs)
|
Vtuber.changeset(vtuber, attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defimpl String.Chars, for: Bright.User do
|
||||||
|
def to_string(%Bright.User{name: name}) when not is_nil(name), do: name
|
||||||
|
def to_string(%Bright.User{}), do: "Anonymous"
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,9 +15,9 @@ defmodule BrightWeb.CoreComponents do
|
||||||
Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage.
|
Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage.
|
||||||
"""
|
"""
|
||||||
use Phoenix.Component
|
use Phoenix.Component
|
||||||
|
use Gettext, backend: BrightWeb.Gettext
|
||||||
|
|
||||||
alias Phoenix.LiveView.JS
|
alias Phoenix.LiveView.JS
|
||||||
import BrightWeb.Gettext
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Renders a modal.
|
Renders a modal.
|
||||||
|
@ -233,10 +233,7 @@ def button(assigns) do
|
||||||
type={@type}
|
type={@type}
|
||||||
class={[
|
class={[
|
||||||
"button",
|
"button",
|
||||||
"is-primary",
|
"is-primary"
|
||||||
"is-rounded",
|
|
||||||
"py-2", "px-3",
|
|
||||||
@class
|
|
||||||
]}
|
]}
|
||||||
{@rest}
|
{@rest}
|
||||||
>
|
>
|
||||||
|
@ -275,6 +272,7 @@ end
|
||||||
attr :id, :any, default: nil
|
attr :id, :any, default: nil
|
||||||
attr :name, :any
|
attr :name, :any
|
||||||
attr :label, :string, default: nil
|
attr :label, :string, default: nil
|
||||||
|
attr :help, :string, default: nil
|
||||||
attr :value, :any
|
attr :value, :any
|
||||||
|
|
||||||
attr :type, :string,
|
attr :type, :string,
|
||||||
|
@ -314,7 +312,7 @@ end
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<div class="field-label is-normal">
|
<div class="field-label is-normal">
|
||||||
<label class="label">
|
<label class="checkbox">
|
||||||
<input type="hidden" name={@name} value="false" disabled={@rest[:disabled]} />
|
<input type="hidden" name={@name} value="false" disabled={@rest[:disabled]} />
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
@ -322,7 +320,6 @@ end
|
||||||
name={@name}
|
name={@name}
|
||||||
value="true"
|
value="true"
|
||||||
checked={@checked}
|
checked={@checked}
|
||||||
class="input"
|
|
||||||
{@rest}
|
{@rest}
|
||||||
/>
|
/>
|
||||||
<%= @label %>
|
<%= @label %>
|
||||||
|
@ -393,6 +390,7 @@ end
|
||||||
]}
|
]}
|
||||||
{@rest}
|
{@rest}
|
||||||
/>
|
/>
|
||||||
|
<.help for={@id}><%= @help %></.help>
|
||||||
<.error :for={msg <- @errors}><%= msg %></.error>
|
<.error :for={msg <- @errors}><%= msg %></.error>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
@ -412,6 +410,19 @@ end
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Renders help text, often found below a form input box
|
||||||
|
"""
|
||||||
|
attr :for, :string, default: nil
|
||||||
|
slot :inner_block, required: true
|
||||||
|
def help(assigns) do
|
||||||
|
~H"""
|
||||||
|
<p for={@for} class="help">
|
||||||
|
<%= render_slot(@inner_block) %>
|
||||||
|
</p>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Generates a generic error message.
|
Generates a generic error message.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -11,10 +11,176 @@ defmodule BrightWeb.AuthController do
|
||||||
plug Ueberauth
|
plug Ueberauth
|
||||||
|
|
||||||
alias Ueberauth.Strategy.Helpers
|
alias Ueberauth.Strategy.Helpers
|
||||||
alias Bright.UserFromAuth
|
alias Bright.{Repo, User}
|
||||||
alias Bright.User
|
|
||||||
|
|
||||||
|
|
||||||
|
@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 = Accounts.generate_user_session_token(user)
|
||||||
|
user_return_to = get_session(conn, :user_return_to)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> renew_session()
|
||||||
|
|> put_token_in_session(token)
|
||||||
|
|> maybe_write_remember_me_cookie(token, params)
|
||||||
|
|> redirect(to: user_return_to || signed_in_path(conn))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_write_remember_me_cookie(conn, token, %{"remember_me" => "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
|
||||||
|
delete_csrf_token()
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> configure_session(renew: true)
|
||||||
|
|> clear_session()
|
||||||
|
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: ~p"/auth/github")
|
||||||
|
|> halt()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Used for routes that require the user to be an administrator.
|
||||||
|
"""
|
||||||
|
def require_admin_user(conn, _opts) do
|
||||||
|
Logger.info("con.assigns[:current_user] as follows. #{inspect(conn.assigns)}")
|
||||||
|
|
||||||
|
case conn.assigns[:current_user] do
|
||||||
|
%User{is_admin: true} -> # Assuming the user struct has an `is_admin` field
|
||||||
|
conn
|
||||||
|
|
||||||
|
%User{} ->
|
||||||
|
conn
|
||||||
|
|> put_flash(:error, "You do not have permission to access this page.")
|
||||||
|
|> redirect(to: ~p"/")
|
||||||
|
|> halt()
|
||||||
|
|
||||||
|
nil ->
|
||||||
|
conn
|
||||||
|
|> put_flash(:error, "You must log in to access this page.")
|
||||||
|
|> maybe_store_return_to()
|
||||||
|
|> redirect(to: ~p"/auth/github")
|
||||||
|
|> 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
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
|
user_token && Accounts.delete_user_session_token(user_token)
|
||||||
|
|
||||||
|
if live_socket_id = get_session(conn, :live_socket_id) do
|
||||||
|
BrightWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
|
||||||
|
end
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> renew_session()
|
||||||
|
|> delete_resp_cookie(@remember_me_cookie)
|
||||||
|
|> redirect(to: ~p"/")
|
||||||
|
end
|
||||||
|
|
||||||
|
# def fetch_current_user(conn) do
|
||||||
|
# conn
|
||||||
|
# |> get_session(:user_id)
|
||||||
|
# |> case do
|
||||||
|
# nil -> nil
|
||||||
|
# user_id -> User.get(user_id)
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
|
||||||
|
defp ensure_user_token(conn) do
|
||||||
|
if token = get_session(conn, :user_token) do
|
||||||
|
{token, conn}
|
||||||
|
else
|
||||||
|
conn = fetch_cookies(conn, signed: [@remember_me_cookie])
|
||||||
|
|
||||||
|
if token = conn.cookies[@remember_me_cookie] do
|
||||||
|
{token, put_token_in_session(conn, token)}
|
||||||
|
else
|
||||||
|
{nil, conn}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
defp put_token_in_session(conn, token) do
|
||||||
|
conn
|
||||||
|
|> put_session(:user_token, token)
|
||||||
|
|> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}")
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def create(conn = %{method: "POST"}, %{"token" => token}) do
|
||||||
|
user = User.get_by_encoded_auth(token)
|
||||||
|
|
||||||
|
if user && Timex.before?(Timex.now(), user.auth_token_expires_at) do
|
||||||
|
sign_in_and_redirect(conn, user, ~p"/~")
|
||||||
|
else
|
||||||
|
conn
|
||||||
|
|> put_flash(:error, "Whoops!")
|
||||||
|
|> render("new.html", user: nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def delete(conn, _params) do
|
def delete(conn, _params) do
|
||||||
conn
|
conn
|
||||||
|> put_flash(:info, "You have been logged out!")
|
|> put_flash(:info, "You have been logged out!")
|
||||||
|
@ -28,30 +194,59 @@ defmodule BrightWeb.AuthController do
|
||||||
|> redirect(to: "/")
|
|> redirect(to: "/")
|
||||||
end
|
end
|
||||||
|
|
||||||
# def callback(conn, %{"provider" => provider}) do
|
|
||||||
# end
|
|
||||||
|
|
||||||
def callback(conn = %{assigns: %{ueberauth_auth: auth}}, _params) do
|
def callback(conn = %{assigns: %{ueberauth_auth: auth}}, _params) do
|
||||||
if user = User.get_by_ueberauth(auth) do
|
|
||||||
sign_in_and_redirect(conn, user, ~p"/~")
|
user_params = %{
|
||||||
else
|
github_handle: Map.get(auth, "nickname", nil),
|
||||||
conn
|
patreon_handle: Map.get(auth, "full_name", nil),
|
||||||
|> put_flash(:success, "Almost there! Please complete your profile now.")
|
name: "test"
|
||||||
|> redirect(to: ~p"/join?#{params_from_ueberauth(auth)}")
|
}
|
||||||
|
|
||||||
|
changeset = User.insert_changeset(%User{}, user_params)
|
||||||
|
|
||||||
|
case Repo.insert(changeset) do
|
||||||
|
{:ok, user} ->
|
||||||
|
UserAuth.log_in_user(conn, user)
|
||||||
|
|
||||||
|
{:error, changeset} ->
|
||||||
|
conn
|
||||||
|
|> put_flash(:error, "Something went wrong. 😭")
|
||||||
|
|> render(:join, changeset: changeset, user: nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# case User.get_by_ueberauth(auth) do
|
||||||
|
# %User{} = user ->
|
||||||
|
# UserAuth.log_in_user(conn, user, %{})
|
||||||
|
|
||||||
|
|
||||||
|
# nil ->
|
||||||
|
# case User.create_from_ueberauth(auth) do
|
||||||
|
# {:ok, %User{} = user} ->
|
||||||
|
# UserAuth.log_in_user(conn, user, %{})
|
||||||
|
|
||||||
|
# {:error, changeset} ->
|
||||||
|
# Logger.error("failed to create user. auth=#{inspect(auth)}")
|
||||||
|
# conn
|
||||||
|
# |> put_flash(:error, "Failed to create user")
|
||||||
|
# |> redirect(to: ~p"/")
|
||||||
|
# end
|
||||||
|
# end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
defp sign_in_and_redirect(conn, user, route) do
|
defp sign_in_and_redirect(conn, user, route) do
|
||||||
|
Logger.info("sign_in_and_redirect with user=#{inspect(user)}")
|
||||||
|
|
||||||
user
|
user
|
||||||
|> User.sign_in_changes()
|
|> User.sign_in_changes()
|
||||||
|> Repo.update()
|
|> Repo.update()
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> assign(:current_user, user)
|
|> assign(:current_user, user)
|
||||||
|> put_flash(:success, "Welcome!")
|
|> put_flash(:success, "Welcome to Futureporn!")
|
||||||
|> put_session("id", user.id)
|
|> put_session("id", user.id)
|
||||||
|> configure_session(renew: true)
|
|> configure_session(renew: true)
|
||||||
|> redirect(to: route)
|
|> redirect(to: route)
|
||||||
|
@ -59,34 +254,14 @@ defmodule BrightWeb.AuthController do
|
||||||
|
|
||||||
|
|
||||||
defp params_from_ueberauth(%{provider: :github, info: info}) do
|
defp params_from_ueberauth(%{provider: :github, info: info}) do
|
||||||
%{name: info.name, handle: info.nickname, github_handle: info.nickname}
|
%{name: info.name, handle: info.nickname, github_handle: info.nickname, github_id: info.uid}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp params_from_ueberauth(%{provider: :patreon, info: info}) do
|
defp params_from_ueberauth(%{provider: :patreon, info: info}) do
|
||||||
%{name: info.name, handle: info.nickname, patreon_handle: info.nickname}
|
%{name: info.name, handle: info.nickname, patreon_handle: info.full_name, patreon_id: info.id}
|
||||||
end
|
end
|
||||||
|
|
||||||
# case Ueberauth.Auth.fetch!(conn) do
|
|
||||||
# {:ok, auth} ->
|
|
||||||
# Logger.info("auth is as follows: #{inspect(auth)}")
|
|
||||||
# {:error, reason} ->
|
|
||||||
# conn
|
|
||||||
# |> put_flash(:error, "Auth failed! #{reason}")
|
|
||||||
# |> redirect(to: ~p"/")
|
|
||||||
# end
|
|
||||||
|
|
||||||
# case UserFromAuth.find_or_create(auth) do
|
defp signed_in_path(_conn), do: ~p"/"
|
||||||
# {:ok, user} ->
|
|
||||||
# conn
|
|
||||||
# |> put_flash(:info, "Successfully authenticated #{inspect(user)}")
|
|
||||||
# |> put_session(:current_user, user)
|
|
||||||
# |> configure_session(renew: true)
|
|
||||||
# |> redirect(to: "/")
|
|
||||||
|
|
||||||
# {:error, reason} ->
|
|
||||||
# conn
|
|
||||||
# |> put_flash(:error, reason)
|
|
||||||
# |> redirect(to: "/")
|
|
||||||
# end
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,15 +1,59 @@
|
||||||
defmodule BrightWeb.UserController do
|
defmodule BrightWeb.UserController do
|
||||||
use BrightWeb, :controller
|
use BrightWeb, :controller
|
||||||
|
alias Bright.{User, Repo}
|
||||||
|
require Logger
|
||||||
|
|
||||||
def index(conn, _params) do
|
def index(conn, _params) do
|
||||||
render(conn, :index)
|
render(conn, :index)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# def show(conn) do
|
||||||
|
# conn
|
||||||
|
# |> render(:show)
|
||||||
|
# end
|
||||||
|
|
||||||
def show(conn) do
|
def show(conn = %{assigns: %{current_user: me}}, _params) do
|
||||||
conn
|
Logger.info(">>> me=#{inspect(me)}")
|
||||||
|> render(:show)
|
render(conn, :show, changeset: User.update_changeset(me))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# def show(conn) do
|
||||||
|
# user = User.get_user!(id)
|
||||||
|
# render(conn, :show, user: user)
|
||||||
|
# end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def join(conn = %{method: "GET"}, params) do
|
||||||
|
user = %User{
|
||||||
|
name: Map.get(params, "name"),
|
||||||
|
github_handle: Map.get(params, "github_handle"),
|
||||||
|
patreon_handle: Map.get(params, "patreon_handle")
|
||||||
|
}
|
||||||
|
|
||||||
|
render(conn, :join, changeset: User.insert_changeset(user), user: nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def join(conn = %{method: "POST"}, params = %{"user" => user_params}) do
|
||||||
|
changeset = User.insert_changeset(%User{}, user_params)
|
||||||
|
|
||||||
|
case Repo.insert(changeset) do
|
||||||
|
{:ok, user} ->
|
||||||
|
welcome_community(conn, user)
|
||||||
|
|
||||||
|
{:error, changeset} ->
|
||||||
|
conn
|
||||||
|
|> put_flash(:error, "Something went wrong. 😭")
|
||||||
|
|> render(:join, changeset: changeset, user: nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp welcome_community(conn, user) do
|
||||||
|
user = User.refresh_auth_token(user)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_flash(:success, "Welcome #{user}")
|
||||||
|
|> redirect(to: ~p"/")
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
<.header>
|
||||||
|
Join Futureporn
|
||||||
|
</.header>
|
||||||
|
|
||||||
|
|
||||||
|
<.user_form changeset={@changeset} action={~p"/join"} />
|
|
@ -0,0 +1,12 @@
|
||||||
|
<.simple_form :let={f} for={@changeset} action={@action}>
|
||||||
|
<.error :if={@changeset.action}>
|
||||||
|
Oops, something went wrong! Please check the errors below.
|
||||||
|
</.error>
|
||||||
|
<.input field={f[:name]} type="text" label="Name" help="This name is displayed publicly to credit you for any contributions" />
|
||||||
|
|
||||||
|
|
||||||
|
<:actions>
|
||||||
|
<.button>Save User Profile</.button>
|
||||||
|
</:actions>
|
||||||
|
</.simple_form>
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
defmodule BrightWeb.UserSessionController do
|
|
||||||
use BrightWeb, :controller
|
|
||||||
|
|
||||||
alias Bright.Accounts
|
|
||||||
alias BrightWeb.UserAuth
|
|
||||||
|
|
||||||
def create(conn, %{"_action" => "registered"} = params) do
|
|
||||||
create(conn, params, "Account created successfully!")
|
|
||||||
end
|
|
||||||
|
|
||||||
def create(conn, %{"_action" => "password_updated"} = params) do
|
|
||||||
conn
|
|
||||||
|> put_session(:user_return_to, ~p"/users/settings")
|
|
||||||
|> create(params, "Password updated successfully!")
|
|
||||||
end
|
|
||||||
|
|
||||||
def create(conn, params) do
|
|
||||||
create(conn, params, "Welcome back!")
|
|
||||||
end
|
|
||||||
|
|
||||||
defp create(conn, %{"user" => user_params}, info) do
|
|
||||||
%{"email" => email, "password" => password} = user_params
|
|
||||||
|
|
||||||
if user = Accounts.get_user_by_email_and_password(email, password) do
|
|
||||||
conn
|
|
||||||
|> put_flash(:info, info)
|
|
||||||
|> UserAuth.log_in_user(user, user_params)
|
|
||||||
else
|
|
||||||
# In order to prevent user enumeration attacks, don't disclose whether the email is registered.
|
|
||||||
conn
|
|
||||||
|> put_flash(:error, "Invalid email or password")
|
|
||||||
|> put_flash(:email, String.slice(email, 0, 160))
|
|
||||||
|> redirect(to: ~p"/users/log_in")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def delete(conn, _params) do
|
|
||||||
conn
|
|
||||||
|> put_flash(:info, "Logged out successfully.")
|
|
||||||
|> UserAuth.log_out_user()
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -20,5 +20,5 @@ defmodule BrightWeb.Gettext do
|
||||||
|
|
||||||
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
|
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
|
||||||
"""
|
"""
|
||||||
use Gettext, otp_app: :bright
|
use Gettext.Backend, otp_app: :bright
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
defmodule BrightWeb.UserConfirmationInstructionsLive do
|
|
||||||
use BrightWeb, :live_view
|
|
||||||
|
|
||||||
alias Bright.Accounts
|
|
||||||
|
|
||||||
def render(assigns) do
|
|
||||||
~H"""
|
|
||||||
<div class="mx-auto max-w-sm">
|
|
||||||
<.header class="text-center">
|
|
||||||
No confirmation instructions received?
|
|
||||||
<:subtitle>We'll send a new confirmation link to your inbox</:subtitle>
|
|
||||||
</.header>
|
|
||||||
|
|
||||||
<.simple_form for={@form} id="resend_confirmation_form" phx-submit="send_instructions">
|
|
||||||
<.input field={@form[:email]} type="email" placeholder="Email" required />
|
|
||||||
<:actions>
|
|
||||||
<.button phx-disable-with="Sending..." class="w-full">
|
|
||||||
Resend confirmation instructions
|
|
||||||
</.button>
|
|
||||||
</:actions>
|
|
||||||
</.simple_form>
|
|
||||||
|
|
||||||
<p class="text-center mt-4">
|
|
||||||
<.link href={~p"/users/register"}>Register</.link>
|
|
||||||
| <.link href={~p"/users/log_in"}>Log in</.link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
def mount(_params, _session, socket) do
|
|
||||||
{:ok, assign(socket, form: to_form(%{}, as: "user"))}
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_event("send_instructions", %{"user" => %{"email" => email}}, socket) do
|
|
||||||
if user = Accounts.get_user_by_email(email) do
|
|
||||||
Accounts.deliver_user_confirmation_instructions(
|
|
||||||
user,
|
|
||||||
&url(~p"/users/confirm/#{&1}")
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
info =
|
|
||||||
"If your email is in our system and it has not been confirmed yet, you will receive an email with instructions shortly."
|
|
||||||
|
|
||||||
{:noreply,
|
|
||||||
socket
|
|
||||||
|> put_flash(:info, info)
|
|
||||||
|> redirect(to: ~p"/")}
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,58 +0,0 @@
|
||||||
defmodule BrightWeb.UserConfirmationLive do
|
|
||||||
use BrightWeb, :live_view
|
|
||||||
|
|
||||||
alias Bright.Accounts
|
|
||||||
|
|
||||||
def render(%{live_action: :edit} = assigns) do
|
|
||||||
~H"""
|
|
||||||
<div class="mx-auto max-w-sm">
|
|
||||||
<.header class="text-center">Confirm Account</.header>
|
|
||||||
|
|
||||||
<.simple_form for={@form} id="confirmation_form" phx-submit="confirm_account">
|
|
||||||
<input type="hidden" name={@form[:token].name} value={@form[:token].value} />
|
|
||||||
<:actions>
|
|
||||||
<.button phx-disable-with="Confirming..." class="w-full">Confirm my account</.button>
|
|
||||||
</:actions>
|
|
||||||
</.simple_form>
|
|
||||||
|
|
||||||
<p class="text-center mt-4">
|
|
||||||
<.link href={~p"/users/register"}>Register</.link>
|
|
||||||
| <.link href={~p"/users/log_in"}>Log in</.link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
def mount(%{"token" => token}, _session, socket) do
|
|
||||||
form = to_form(%{"token" => token}, as: "user")
|
|
||||||
{:ok, assign(socket, form: form), temporary_assigns: [form: nil]}
|
|
||||||
end
|
|
||||||
|
|
||||||
# Do not log in the user after confirmation to avoid a
|
|
||||||
# leaked token giving the user access to the account.
|
|
||||||
def handle_event("confirm_account", %{"user" => %{"token" => token}}, socket) do
|
|
||||||
case Accounts.confirm_user(token) do
|
|
||||||
{:ok, _} ->
|
|
||||||
{:noreply,
|
|
||||||
socket
|
|
||||||
|> put_flash(:info, "User confirmed successfully.")
|
|
||||||
|> redirect(to: ~p"/")}
|
|
||||||
|
|
||||||
: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 socket.assigns do
|
|
||||||
%{current_user: %{confirmed_at: confirmed_at}} when not is_nil(confirmed_at) ->
|
|
||||||
{:noreply, redirect(socket, to: ~p"/")}
|
|
||||||
|
|
||||||
%{} ->
|
|
||||||
{:noreply,
|
|
||||||
socket
|
|
||||||
|> put_flash(:error, "User confirmation link is invalid or it has expired.")
|
|
||||||
|> redirect(to: ~p"/")}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,50 +0,0 @@
|
||||||
defmodule BrightWeb.UserForgotPasswordLive do
|
|
||||||
use BrightWeb, :live_view
|
|
||||||
|
|
||||||
alias Bright.Accounts
|
|
||||||
|
|
||||||
def render(assigns) do
|
|
||||||
~H"""
|
|
||||||
<div class="mx-auto max-w-sm">
|
|
||||||
<.header class="text-center">
|
|
||||||
Forgot your password?
|
|
||||||
<:subtitle>We'll send a password reset link to your inbox</:subtitle>
|
|
||||||
</.header>
|
|
||||||
|
|
||||||
<.simple_form for={@form} id="reset_password_form" phx-submit="send_email">
|
|
||||||
<.input field={@form[:email]} type="email" placeholder="Email" required />
|
|
||||||
<:actions>
|
|
||||||
<.button phx-disable-with="Sending..." class="w-full">
|
|
||||||
Send password reset instructions
|
|
||||||
</.button>
|
|
||||||
</:actions>
|
|
||||||
</.simple_form>
|
|
||||||
<p class="text-center text-sm mt-4">
|
|
||||||
<.link href={~p"/users/register"}>Register</.link>
|
|
||||||
| <.link href={~p"/users/log_in"}>Log in</.link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
def mount(_params, _session, socket) do
|
|
||||||
{:ok, assign(socket, form: to_form(%{}, as: "user"))}
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_event("send_email", %{"user" => %{"email" => email}}, socket) do
|
|
||||||
if user = Accounts.get_user_by_email(email) do
|
|
||||||
Accounts.deliver_user_reset_password_instructions(
|
|
||||||
user,
|
|
||||||
&url(~p"/users/reset_password/#{&1}")
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
info =
|
|
||||||
"If your email is in our system, you will receive instructions to reset your password shortly."
|
|
||||||
|
|
||||||
{:noreply,
|
|
||||||
socket
|
|
||||||
|> put_flash(:info, info)
|
|
||||||
|> redirect(to: ~p"/")}
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,43 +0,0 @@
|
||||||
defmodule BrightWeb.UserLoginLive do
|
|
||||||
use BrightWeb, :live_view
|
|
||||||
|
|
||||||
def render(assigns) do
|
|
||||||
~H"""
|
|
||||||
<div class="mx-auto max-w-sm">
|
|
||||||
<.header class="text-center">
|
|
||||||
Log in to account
|
|
||||||
<:subtitle>
|
|
||||||
Don't have an account?
|
|
||||||
<.link navigate={~p"/users/register"} class="font-semibold text-brand hover:underline">
|
|
||||||
Sign up
|
|
||||||
</.link>
|
|
||||||
for an account now.
|
|
||||||
</:subtitle>
|
|
||||||
</.header>
|
|
||||||
|
|
||||||
<.simple_form for={@form} id="login_form" action={~p"/users/log_in"} phx-update="ignore">
|
|
||||||
<.input field={@form[:email]} type="email" label="Email" required />
|
|
||||||
<.input field={@form[:password]} type="password" label="Password" required />
|
|
||||||
|
|
||||||
<:actions>
|
|
||||||
<.input field={@form[:remember_me]} type="checkbox" label="Keep me logged in" />
|
|
||||||
<.link href={~p"/users/reset_password"} class="text-sm font-semibold">
|
|
||||||
Forgot your password?
|
|
||||||
</.link>
|
|
||||||
</:actions>
|
|
||||||
<:actions>
|
|
||||||
<.button phx-disable-with="Logging in..." class="w-full">
|
|
||||||
Log in <span aria-hidden="true">→</span>
|
|
||||||
</.button>
|
|
||||||
</:actions>
|
|
||||||
</.simple_form>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
def mount(_params, _session, socket) do
|
|
||||||
email = Phoenix.Flash.get(socket.assigns.flash, :email)
|
|
||||||
form = to_form(%{"email" => email}, as: "user")
|
|
||||||
{:ok, assign(socket, form: form), temporary_assigns: [form: form]}
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,87 +0,0 @@
|
||||||
defmodule BrightWeb.UserRegistrationLive do
|
|
||||||
use BrightWeb, :live_view
|
|
||||||
|
|
||||||
alias Bright.Accounts
|
|
||||||
alias Bright.Accounts.User
|
|
||||||
|
|
||||||
def render(assigns) do
|
|
||||||
~H"""
|
|
||||||
<div class="mx-auto max-w-sm">
|
|
||||||
<.header class="text-center">
|
|
||||||
Register for an account
|
|
||||||
<:subtitle>
|
|
||||||
Already registered?
|
|
||||||
<.link navigate={~p"/users/log_in"} class="font-semibold text-brand hover:underline">
|
|
||||||
Log in
|
|
||||||
</.link>
|
|
||||||
to your account now.
|
|
||||||
</:subtitle>
|
|
||||||
</.header>
|
|
||||||
|
|
||||||
<.simple_form
|
|
||||||
for={@form}
|
|
||||||
id="registration_form"
|
|
||||||
phx-submit="save"
|
|
||||||
phx-change="validate"
|
|
||||||
phx-trigger-action={@trigger_submit}
|
|
||||||
action={~p"/users/log_in?_action=registered"}
|
|
||||||
method="post"
|
|
||||||
>
|
|
||||||
<.error :if={@check_errors}>
|
|
||||||
Oops, something went wrong! Please check the errors below.
|
|
||||||
</.error>
|
|
||||||
|
|
||||||
<.input field={@form[:email]} type="email" label="Email" required />
|
|
||||||
<.input field={@form[:password]} type="password" label="Password" required />
|
|
||||||
|
|
||||||
<:actions>
|
|
||||||
<.button phx-disable-with="Creating account..." class="w-full">Create an account</.button>
|
|
||||||
</:actions>
|
|
||||||
</.simple_form>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
def mount(_params, _session, socket) do
|
|
||||||
changeset = Accounts.change_user_registration(%User{})
|
|
||||||
|
|
||||||
socket =
|
|
||||||
socket
|
|
||||||
|> assign(trigger_submit: false, check_errors: false)
|
|
||||||
|> assign_form(changeset)
|
|
||||||
|
|
||||||
{:ok, socket, temporary_assigns: [form: nil]}
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_event("save", %{"user" => user_params}, socket) do
|
|
||||||
case Accounts.register_user(user_params) do
|
|
||||||
{:ok, user} ->
|
|
||||||
{:ok, _} =
|
|
||||||
Accounts.deliver_user_confirmation_instructions(
|
|
||||||
user,
|
|
||||||
&url(~p"/users/confirm/#{&1}")
|
|
||||||
)
|
|
||||||
|
|
||||||
changeset = Accounts.change_user_registration(user)
|
|
||||||
{:noreply, socket |> assign(trigger_submit: true) |> assign_form(changeset)}
|
|
||||||
|
|
||||||
{:error, %Ecto.Changeset{} = changeset} ->
|
|
||||||
{:noreply, socket |> assign(check_errors: true) |> assign_form(changeset)}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_event("validate", %{"user" => user_params}, socket) do
|
|
||||||
changeset = Accounts.change_user_registration(%User{}, user_params)
|
|
||||||
{:noreply, assign_form(socket, Map.put(changeset, :action, :validate))}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp assign_form(socket, %Ecto.Changeset{} = changeset) do
|
|
||||||
form = to_form(changeset, as: "user")
|
|
||||||
|
|
||||||
if changeset.valid? do
|
|
||||||
assign(socket, form: form, check_errors: false)
|
|
||||||
else
|
|
||||||
assign(socket, form: form)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,89 +0,0 @@
|
||||||
defmodule BrightWeb.UserResetPasswordLive do
|
|
||||||
use BrightWeb, :live_view
|
|
||||||
|
|
||||||
alias Bright.Accounts
|
|
||||||
|
|
||||||
def render(assigns) do
|
|
||||||
~H"""
|
|
||||||
<div class="mx-auto max-w-sm">
|
|
||||||
<.header class="text-center">Reset Password</.header>
|
|
||||||
|
|
||||||
<.simple_form
|
|
||||||
for={@form}
|
|
||||||
id="reset_password_form"
|
|
||||||
phx-submit="reset_password"
|
|
||||||
phx-change="validate"
|
|
||||||
>
|
|
||||||
<.error :if={@form.errors != []}>
|
|
||||||
Oops, something went wrong! Please check the errors below.
|
|
||||||
</.error>
|
|
||||||
|
|
||||||
<.input field={@form[:password]} type="password" label="New password" required />
|
|
||||||
<.input
|
|
||||||
field={@form[:password_confirmation]}
|
|
||||||
type="password"
|
|
||||||
label="Confirm new password"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<:actions>
|
|
||||||
<.button phx-disable-with="Resetting..." class="w-full">Reset Password</.button>
|
|
||||||
</:actions>
|
|
||||||
</.simple_form>
|
|
||||||
|
|
||||||
<p class="text-center text-sm mt-4">
|
|
||||||
<.link href={~p"/users/register"}>Register</.link>
|
|
||||||
| <.link href={~p"/users/log_in"}>Log in</.link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
def mount(params, _session, socket) do
|
|
||||||
socket = assign_user_and_token(socket, params)
|
|
||||||
|
|
||||||
form_source =
|
|
||||||
case socket.assigns do
|
|
||||||
%{user: user} ->
|
|
||||||
Accounts.change_user_password(user)
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
%{}
|
|
||||||
end
|
|
||||||
|
|
||||||
{:ok, assign_form(socket, form_source), temporary_assigns: [form: nil]}
|
|
||||||
end
|
|
||||||
|
|
||||||
# Do not log in the user after reset password to avoid a
|
|
||||||
# leaked token giving the user access to the account.
|
|
||||||
def handle_event("reset_password", %{"user" => user_params}, socket) do
|
|
||||||
case Accounts.reset_user_password(socket.assigns.user, user_params) do
|
|
||||||
{:ok, _} ->
|
|
||||||
{:noreply,
|
|
||||||
socket
|
|
||||||
|> put_flash(:info, "Password reset successfully.")
|
|
||||||
|> redirect(to: ~p"/users/log_in")}
|
|
||||||
|
|
||||||
{:error, changeset} ->
|
|
||||||
{:noreply, assign_form(socket, Map.put(changeset, :action, :insert))}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_event("validate", %{"user" => user_params}, socket) do
|
|
||||||
changeset = Accounts.change_user_password(socket.assigns.user, user_params)
|
|
||||||
{:noreply, assign_form(socket, Map.put(changeset, :action, :validate))}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp assign_user_and_token(socket, %{"token" => token}) do
|
|
||||||
if user = Accounts.get_user_by_reset_password_token(token) do
|
|
||||||
assign(socket, user: user, token: token)
|
|
||||||
else
|
|
||||||
socket
|
|
||||||
|> put_flash(:error, "Reset password link is invalid or it has expired.")
|
|
||||||
|> redirect(to: ~p"/")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp assign_form(socket, %{} = source) do
|
|
||||||
assign(socket, :form, to_form(source, as: "user"))
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,167 +0,0 @@
|
||||||
defmodule BrightWeb.UserSettingsLive do
|
|
||||||
use BrightWeb, :live_view
|
|
||||||
|
|
||||||
alias Bright.Accounts
|
|
||||||
|
|
||||||
def render(assigns) do
|
|
||||||
~H"""
|
|
||||||
<.header class="text-center">
|
|
||||||
Account Settings
|
|
||||||
<:subtitle>Manage your account email address and password settings</:subtitle>
|
|
||||||
</.header>
|
|
||||||
|
|
||||||
<div class="space-y-12 divide-y">
|
|
||||||
<div>
|
|
||||||
<.simple_form
|
|
||||||
for={@email_form}
|
|
||||||
id="email_form"
|
|
||||||
phx-submit="update_email"
|
|
||||||
phx-change="validate_email"
|
|
||||||
>
|
|
||||||
<.input field={@email_form[:email]} type="email" label="Email" required />
|
|
||||||
<.input
|
|
||||||
field={@email_form[:current_password]}
|
|
||||||
name="current_password"
|
|
||||||
id="current_password_for_email"
|
|
||||||
type="password"
|
|
||||||
label="Current password"
|
|
||||||
value={@email_form_current_password}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<:actions>
|
|
||||||
<.button phx-disable-with="Changing...">Change Email</.button>
|
|
||||||
</:actions>
|
|
||||||
</.simple_form>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<.simple_form
|
|
||||||
for={@password_form}
|
|
||||||
id="password_form"
|
|
||||||
action={~p"/users/log_in?_action=password_updated"}
|
|
||||||
method="post"
|
|
||||||
phx-change="validate_password"
|
|
||||||
phx-submit="update_password"
|
|
||||||
phx-trigger-action={@trigger_submit}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
name={@password_form[:email].name}
|
|
||||||
type="hidden"
|
|
||||||
id="hidden_user_email"
|
|
||||||
value={@current_email}
|
|
||||||
/>
|
|
||||||
<.input field={@password_form[:password]} type="password" label="New password" required />
|
|
||||||
<.input
|
|
||||||
field={@password_form[:password_confirmation]}
|
|
||||||
type="password"
|
|
||||||
label="Confirm new password"
|
|
||||||
/>
|
|
||||||
<.input
|
|
||||||
field={@password_form[:current_password]}
|
|
||||||
name="current_password"
|
|
||||||
type="password"
|
|
||||||
label="Current password"
|
|
||||||
id="current_password_for_password"
|
|
||||||
value={@current_password}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<:actions>
|
|
||||||
<.button phx-disable-with="Changing...">Change Password</.button>
|
|
||||||
</:actions>
|
|
||||||
</.simple_form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
def mount(%{"token" => token}, _session, socket) do
|
|
||||||
socket =
|
|
||||||
case Accounts.update_user_email(socket.assigns.current_user, token) do
|
|
||||||
:ok ->
|
|
||||||
put_flash(socket, :info, "Email changed successfully.")
|
|
||||||
|
|
||||||
:error ->
|
|
||||||
put_flash(socket, :error, "Email change link is invalid or it has expired.")
|
|
||||||
end
|
|
||||||
|
|
||||||
{:ok, push_navigate(socket, to: ~p"/users/settings")}
|
|
||||||
end
|
|
||||||
|
|
||||||
def mount(_params, _session, socket) do
|
|
||||||
user = socket.assigns.current_user
|
|
||||||
email_changeset = Accounts.change_user_email(user)
|
|
||||||
password_changeset = Accounts.change_user_password(user)
|
|
||||||
|
|
||||||
socket =
|
|
||||||
socket
|
|
||||||
|> assign(:current_password, nil)
|
|
||||||
|> assign(:email_form_current_password, nil)
|
|
||||||
|> assign(:current_email, user.email)
|
|
||||||
|> assign(:email_form, to_form(email_changeset))
|
|
||||||
|> assign(:password_form, to_form(password_changeset))
|
|
||||||
|> assign(:trigger_submit, false)
|
|
||||||
|
|
||||||
{:ok, socket}
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_event("validate_email", params, socket) do
|
|
||||||
%{"current_password" => password, "user" => user_params} = params
|
|
||||||
|
|
||||||
email_form =
|
|
||||||
socket.assigns.current_user
|
|
||||||
|> Accounts.change_user_email(user_params)
|
|
||||||
|> Map.put(:action, :validate)
|
|
||||||
|> to_form()
|
|
||||||
|
|
||||||
{:noreply, assign(socket, email_form: email_form, email_form_current_password: password)}
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_event("update_email", params, socket) do
|
|
||||||
%{"current_password" => password, "user" => user_params} = params
|
|
||||||
user = socket.assigns.current_user
|
|
||||||
|
|
||||||
case Accounts.apply_user_email(user, password, user_params) do
|
|
||||||
{:ok, applied_user} ->
|
|
||||||
Accounts.deliver_user_update_email_instructions(
|
|
||||||
applied_user,
|
|
||||||
user.email,
|
|
||||||
&url(~p"/users/settings/confirm_email/#{&1}")
|
|
||||||
)
|
|
||||||
|
|
||||||
info = "A link to confirm your email change has been sent to the new address."
|
|
||||||
{:noreply, socket |> put_flash(:info, info) |> assign(email_form_current_password: nil)}
|
|
||||||
|
|
||||||
{:error, changeset} ->
|
|
||||||
{:noreply, assign(socket, :email_form, to_form(Map.put(changeset, :action, :insert)))}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_event("validate_password", params, socket) do
|
|
||||||
%{"current_password" => password, "user" => user_params} = params
|
|
||||||
|
|
||||||
password_form =
|
|
||||||
socket.assigns.current_user
|
|
||||||
|> Accounts.change_user_password(user_params)
|
|
||||||
|> Map.put(:action, :validate)
|
|
||||||
|> to_form()
|
|
||||||
|
|
||||||
{:noreply, assign(socket, password_form: password_form, current_password: password)}
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_event("update_password", params, socket) do
|
|
||||||
%{"current_password" => password, "user" => user_params} = params
|
|
||||||
user = socket.assigns.current_user
|
|
||||||
|
|
||||||
case Accounts.update_user_password(user, password, user_params) do
|
|
||||||
{:ok, user} ->
|
|
||||||
password_form =
|
|
||||||
user
|
|
||||||
|> Accounts.change_user_password(user_params)
|
|
||||||
|> to_form()
|
|
||||||
|
|
||||||
{:noreply, assign(socket, trigger_submit: true, password_form: password_form)}
|
|
||||||
|
|
||||||
{:error, changeset} ->
|
|
||||||
{:noreply, assign(socket, password_form: to_form(changeset))}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,57 +1,56 @@
|
||||||
defmodule BrightWeb.Router do
|
defmodule BrightWeb.Router do
|
||||||
use BrightWeb, :router
|
use BrightWeb, :router
|
||||||
|
|
||||||
import BrightWeb.UserAuth
|
import BrightWeb.AuthController
|
||||||
|
|
||||||
|
|
||||||
pipeline :browser do
|
pipeline :browser do
|
||||||
plug :accepts, ["html", "json"]
|
plug(:accepts, ["html", "json"])
|
||||||
plug :fetch_session
|
plug(:fetch_session)
|
||||||
plug :fetch_live_flash
|
plug(:fetch_live_flash)
|
||||||
plug :put_root_layout, html: {BrightWeb.Layouts, :root}
|
plug(:put_root_layout, html: {BrightWeb.Layouts, :root})
|
||||||
plug :protect_from_forgery
|
plug(:protect_from_forgery)
|
||||||
plug :put_secure_browser_headers
|
plug(:put_secure_browser_headers)
|
||||||
plug :fetch_current_user
|
plug(:fetch_current_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp fetch_current_user(conn, _) do
|
||||||
|
if user_uuid = get_session(conn, :current_user) do
|
||||||
|
assign(conn, :current_user, user_uuid)
|
||||||
|
else
|
||||||
|
conn
|
||||||
|
|> assign(:current_user, nil)
|
||||||
|
|> put_session(:current_user, nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
pipeline :api do
|
pipeline :api do
|
||||||
plug :accepts, ["json"]
|
plug(:accepts, ["json"])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
scope "/" do
|
scope "/" do
|
||||||
pipe_through [:browser, :require_authenticated_user, :require_admin_user]
|
pipe_through([:browser, :require_authenticated_user, :require_admin_user])
|
||||||
## !!! DANGER, platforms must only be writable by admins, (unless we implement SVG sanitizing)
|
## !!! DANGER, platforms must only be writable by admins, (unless we implement SVG sanitizing)
|
||||||
get "/platforms/new", PlatformController, :new
|
get("/platforms/new", PlatformController, :new)
|
||||||
post "/platforms", PlatformController, :create
|
post("/platforms", PlatformController, :create)
|
||||||
get "/platforms/:id/edit", PlatformController, :edit
|
get("/platforms/:id/edit", PlatformController, :edit)
|
||||||
patch "/platforms/:id", PlatformController, :update
|
patch("/platforms/:id", PlatformController, :update)
|
||||||
put "/platforms/:id", PlatformController, :update
|
put("/platforms/:id", PlatformController, :update)
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/auth", BrightWeb do
|
scope "/auth", BrightWeb do
|
||||||
pipe_through :browser
|
pipe_through(:browser)
|
||||||
|
|
||||||
get "/:provider", AuthController, :request
|
get("/:provider", AuthController, :request)
|
||||||
get "/:provider/callback", AuthController, :callback
|
get("/:provider/callback", AuthController, :callback)
|
||||||
post "/:provider/callback", AuthController, :callback
|
post("/:provider/callback", AuthController, :callback)
|
||||||
delete "/logout", AuthController, :delete
|
delete("/logout", AuthController, :delete)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
scope "/" do
|
scope "/" do
|
||||||
pipe_through [:browser, :require_authenticated_user]
|
pipe_through([:browser, :require_authenticated_user])
|
||||||
|
|
||||||
get "/streams/new", StreamController, :new
|
get("/streams/new", StreamController, :new)
|
||||||
post "/streams", StreamController, :create
|
post("/streams", StreamController, :create)
|
||||||
|
|
||||||
# get "/vods/new", VodController, :new
|
# get "/vods/new", VodController, :new
|
||||||
# post "/vods", VodController, :create
|
# post "/vods", VodController, :create
|
||||||
|
@ -69,65 +68,54 @@ defmodule BrightWeb.Router do
|
||||||
# get "/vtubers/:id/edit", VtuberController, :edit
|
# get "/vtubers/:id/edit", VtuberController, :edit
|
||||||
# end
|
# end
|
||||||
|
|
||||||
get "/profile", UserController, :show
|
get("/tags/new", TagController, :new)
|
||||||
|
post("/tags", TagController, :create)
|
||||||
|
|
||||||
get "/tags/new", TagController, :new
|
|
||||||
post "/tags", TagController, :create
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
scope "/", BrightWeb do
|
scope "/", BrightWeb do
|
||||||
pipe_through :browser
|
pipe_through(:browser)
|
||||||
|
|
||||||
get "/", PageController, :home
|
get("/", PageController, :home)
|
||||||
|
|
||||||
get "/patrons", PatronController, :index
|
get("/profile", UserController, :show, as: :user)
|
||||||
get "/about", PageController, :about
|
|
||||||
get "/api", PageController, :api
|
|
||||||
|
|
||||||
|
get("/patrons", PatronController, :index)
|
||||||
|
get("/about", PageController, :about)
|
||||||
|
get("/api", PageController, :api)
|
||||||
|
|
||||||
resources "/orders", OrderController, only: [:create, :show]
|
get("/join", UserController, :join)
|
||||||
|
post("/join", UserController, :join)
|
||||||
|
post("/join", UserController, :join)
|
||||||
|
|
||||||
get "/streams", StreamController, :index
|
resources("/orders", OrderController, only: [:create, :show])
|
||||||
get "/streams/:id", StreamController, :show
|
|
||||||
|
|
||||||
|
get("/streams", StreamController, :index)
|
||||||
|
get("/streams/:id", StreamController, :show)
|
||||||
|
|
||||||
resources "/vods", VodController
|
resources("/vods", VodController)
|
||||||
get "/vods/:id", VodController, :show
|
get("/vods/:id", VodController, :show)
|
||||||
get "/vods", VodController, :index
|
get("/vods", VodController, :index)
|
||||||
|
|
||||||
|
get("/tags", TagController, :index)
|
||||||
|
get("/tags:id", TagController, :show)
|
||||||
|
|
||||||
get "/tags", TagController, :index
|
get("/platforms", PlatformController, :index)
|
||||||
get "/tags:id", TagController, :show
|
get("/platforms/:id", PlatformController, :show)
|
||||||
|
|
||||||
get "/platforms", PlatformController, :index
|
get("/vtubers", VtuberController, :index)
|
||||||
get "/platforms/:id", PlatformController, :show
|
get("/vtubers/:id", VtuberController, :show)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
get "/vtubers", VtuberController, :index
|
|
||||||
get "/vtubers/:id", VtuberController, :show
|
|
||||||
|
|
||||||
resources "/vt", VtuberController do
|
resources "/vt", VtuberController do
|
||||||
get "/vods", VodController, :index
|
get("/vods", VodController, :index)
|
||||||
get "/vods/:id", VodController, :show
|
get("/vods/:id", VodController, :show)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Other scopes may use custom stacks.
|
# Other scopes may use custom stacks.
|
||||||
scope "/api", BrightWeb do
|
scope "/api", BrightWeb do
|
||||||
pipe_through :api
|
pipe_through(:api)
|
||||||
resources "/urls", UrlController, except: [:new, :edit]
|
resources("/urls", UrlController, except: [:new, :edit])
|
||||||
get "/health", PageController, :health
|
get("/health", PageController, :health)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Enable LiveDashboard and Swoosh mailbox preview in development
|
# Enable LiveDashboard and Swoosh mailbox preview in development
|
||||||
|
@ -140,23 +128,19 @@ defmodule BrightWeb.Router do
|
||||||
import Phoenix.LiveDashboard.Router
|
import Phoenix.LiveDashboard.Router
|
||||||
|
|
||||||
scope "/dev" do
|
scope "/dev" do
|
||||||
pipe_through :browser
|
pipe_through(:browser)
|
||||||
|
|
||||||
live_dashboard "/dashboard", metrics: BrightWeb.Telemetry
|
live_dashboard("/dashboard", metrics: BrightWeb.Telemetry)
|
||||||
forward "/mailbox", Plug.Swoosh.MailboxPreview
|
forward("/mailbox", Plug.Swoosh.MailboxPreview)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
## Authentication routes
|
## Authentication routes
|
||||||
|
|
||||||
scope "/", BrightWeb do
|
scope "/", BrightWeb do
|
||||||
pipe_through [:browser]
|
pipe_through([:browser])
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Authentication routes
|
## Authentication routes
|
||||||
|
|
||||||
# scope "/", BrightWeb do
|
# scope "/", BrightWeb do
|
||||||
|
@ -173,25 +157,25 @@ defmodule BrightWeb.Router do
|
||||||
# post "/users/log_in", UserSessionController, :create
|
# post "/users/log_in", UserSessionController, :create
|
||||||
# end
|
# end
|
||||||
|
|
||||||
scope "/", BrightWeb do
|
# scope "/", BrightWeb do
|
||||||
pipe_through [:browser, :require_authenticated_user]
|
# pipe_through [:browser, :require_authenticated_user]
|
||||||
|
|
||||||
live_session :require_authenticated_user,
|
# live_session :require_authenticated_user,
|
||||||
on_mount: [{BrightWeb.UserAuth, :ensure_authenticated}] do
|
# on_mount: [{BrightWeb.UserAuth, :ensure_authenticated}] do
|
||||||
live "/users/settings", UserSettingsLive, :edit
|
# live "/users/settings", UserSettingsLive, :edit
|
||||||
live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
|
# live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
|
||||||
end
|
# end
|
||||||
end
|
# end
|
||||||
|
|
||||||
scope "/", BrightWeb do
|
# scope "/", BrightWeb do
|
||||||
pipe_through [:browser]
|
# pipe_through [:browser]
|
||||||
|
|
||||||
delete "/users/log_out", UserSessionController, :delete
|
# delete "/users/log_out", UserSessionController, :delete
|
||||||
|
|
||||||
live_session :current_user,
|
# live_session :current_user,
|
||||||
on_mount: [{BrightWeb.UserAuth, :mount_current_user}] do
|
# on_mount: [{BrightWeb.UserAuth, :mount_current_user}] do
|
||||||
live "/users/confirm/:token", UserConfirmationLive, :edit
|
# live "/users/confirm/:token", UserConfirmationLive, :edit
|
||||||
live "/users/confirm", UserConfirmationInstructionsLive, :new
|
# live "/users/confirm", UserConfirmationInstructionsLive, :new
|
||||||
end
|
# end
|
||||||
end
|
# end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,256 +0,0 @@
|
||||||
defmodule BrightWeb.UserAuth do
|
|
||||||
use BrightWeb, :verified_routes
|
|
||||||
|
|
||||||
import Plug.Conn
|
|
||||||
import Phoenix.Controller
|
|
||||||
|
|
||||||
alias Bright.Accounts
|
|
||||||
alias Bright.User
|
|
||||||
|
|
||||||
# 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 "_bright_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 = Accounts.generate_user_session_token(user)
|
|
||||||
user_return_to = get_session(conn, :user_return_to)
|
|
||||||
|
|
||||||
conn
|
|
||||||
|> renew_session()
|
|
||||||
|> put_token_in_session(token)
|
|
||||||
|> maybe_write_remember_me_cookie(token, params)
|
|
||||||
|> redirect(to: user_return_to || signed_in_path(conn))
|
|
||||||
end
|
|
||||||
|
|
||||||
defp maybe_write_remember_me_cookie(conn, token, %{"remember_me" => "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
|
|
||||||
delete_csrf_token()
|
|
||||||
|
|
||||||
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)
|
|
||||||
user_token && Accounts.delete_user_session_token(user_token)
|
|
||||||
|
|
||||||
if live_socket_id = get_session(conn, :live_socket_id) do
|
|
||||||
BrightWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
|
|
||||||
end
|
|
||||||
|
|
||||||
conn
|
|
||||||
|> renew_session()
|
|
||||||
|> delete_resp_cookie(@remember_me_cookie)
|
|
||||||
|> redirect(to: ~p"/")
|
|
||||||
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)
|
|
||||||
user = user_token && Accounts.get_user_by_session_token(user_token)
|
|
||||||
assign(conn, :current_user, user)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp ensure_user_token(conn) do
|
|
||||||
if token = get_session(conn, :user_token) do
|
|
||||||
{token, conn}
|
|
||||||
else
|
|
||||||
conn = fetch_cookies(conn, signed: [@remember_me_cookie])
|
|
||||||
|
|
||||||
if token = conn.cookies[@remember_me_cookie] do
|
|
||||||
{token, put_token_in_session(conn, token)}
|
|
||||||
else
|
|
||||||
{nil, conn}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Handles mounting and authenticating the current_user in LiveViews.
|
|
||||||
|
|
||||||
## `on_mount` arguments
|
|
||||||
|
|
||||||
* `:mount_current_user` - Assigns current_user
|
|
||||||
to socket assigns based on user_token, or nil if
|
|
||||||
there's no user_token or no matching user.
|
|
||||||
|
|
||||||
* `:ensure_authenticated` - Authenticates the user from the session,
|
|
||||||
and assigns the current_user to socket assigns based
|
|
||||||
on user_token.
|
|
||||||
Redirects to login page if there's no logged user.
|
|
||||||
|
|
||||||
* `:redirect_if_user_is_authenticated` - Authenticates the user from the session.
|
|
||||||
Redirects to signed_in_path if there's a logged user.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
Use the `on_mount` lifecycle macro in LiveViews to mount or authenticate
|
|
||||||
the current_user:
|
|
||||||
|
|
||||||
defmodule BrightWeb.PageLive do
|
|
||||||
use BrightWeb, :live_view
|
|
||||||
|
|
||||||
on_mount {BrightWeb.UserAuth, :mount_current_user}
|
|
||||||
...
|
|
||||||
end
|
|
||||||
|
|
||||||
Or use the `live_session` of your router to invoke the on_mount callback:
|
|
||||||
|
|
||||||
live_session :authenticated, on_mount: [{BrightWeb.UserAuth, :ensure_authenticated}] do
|
|
||||||
live "/profile", ProfileLive, :index
|
|
||||||
end
|
|
||||||
"""
|
|
||||||
def on_mount(:mount_current_user, _params, session, socket) do
|
|
||||||
{:cont, mount_current_user(socket, session)}
|
|
||||||
end
|
|
||||||
|
|
||||||
def on_mount(:ensure_authenticated, _params, session, socket) do
|
|
||||||
socket = mount_current_user(socket, session)
|
|
||||||
|
|
||||||
if socket.assigns.current_user do
|
|
||||||
{:cont, socket}
|
|
||||||
else
|
|
||||||
socket =
|
|
||||||
socket
|
|
||||||
|> Phoenix.LiveView.put_flash(:error, "You must log in to access this page.")
|
|
||||||
|> Phoenix.LiveView.redirect(to: ~p"/users/log_in")
|
|
||||||
|
|
||||||
{:halt, socket}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def on_mount(:redirect_if_user_is_authenticated, _params, session, socket) do
|
|
||||||
socket = mount_current_user(socket, session)
|
|
||||||
|
|
||||||
if socket.assigns.current_user do
|
|
||||||
{:halt, Phoenix.LiveView.redirect(socket, to: signed_in_path(socket))}
|
|
||||||
else
|
|
||||||
{:cont, socket}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp mount_current_user(socket, session) do
|
|
||||||
Phoenix.Component.assign_new(socket, :current_user, fn ->
|
|
||||||
if user_token = session["user_token"] do
|
|
||||||
Accounts.get_user_by_session_token(user_token)
|
|
||||||
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: ~p"/auth/github")
|
|
||||||
|> halt()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Used for routes that require the user to be an administrator.
|
|
||||||
"""
|
|
||||||
def require_admin_user(conn, _opts) do
|
|
||||||
Logger.info("con.assigns[:current_user] as follows. #{inspect(conn.assigns)}")
|
|
||||||
|
|
||||||
case conn.assigns[:current_user] do
|
|
||||||
%User{is_admin: true} -> # Assuming the user struct has an `is_admin` field
|
|
||||||
conn
|
|
||||||
|
|
||||||
%User{} ->
|
|
||||||
conn
|
|
||||||
|> put_flash(:error, "You do not have permission to access this page.")
|
|
||||||
|> redirect(to: ~p"/")
|
|
||||||
|> halt()
|
|
||||||
|
|
||||||
nil ->
|
|
||||||
conn
|
|
||||||
|> put_flash(:error, "You must log in to access this page.")
|
|
||||||
|> maybe_store_return_to()
|
|
||||||
|> redirect(to: ~p"/auth/github")
|
|
||||||
|> halt()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
defp put_token_in_session(conn, token) do
|
|
||||||
conn
|
|
||||||
|> put_session(:user_token, token)
|
|
||||||
|> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}")
|
|
||||||
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: ~p"/"
|
|
||||||
end
|
|
|
@ -60,6 +60,10 @@ defmodule Bright.MixProject do
|
||||||
{:ueberauth, "~> 0.7.0"},
|
{:ueberauth, "~> 0.7.0"},
|
||||||
{:ueberauth_github, "~> 0.8"},
|
{:ueberauth_github, "~> 0.8"},
|
||||||
{:timex, "~> 3.0"},
|
{:timex, "~> 3.0"},
|
||||||
|
{:ex_aws_s3, "~> 2.0"},
|
||||||
|
{:ex_aws, "~> 2.1"},
|
||||||
|
{:ffmpex, "~> 0.11.0"},
|
||||||
|
{:sweet_xml, "~> 0.6"}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,10 @@
|
||||||
"ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"},
|
"ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"},
|
||||||
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
|
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
|
||||||
"esbuild": {:hex, :esbuild, "0.8.2", "5f379dfa383ef482b738e7771daf238b2d1cfb0222bef9d3b20d4c8f06c7a7ac", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "558a8a08ed78eb820efbfda1de196569d8bfa9b51e8371a1934fbb31345feda7"},
|
"esbuild": {:hex, :esbuild, "0.8.2", "5f379dfa383ef482b738e7771daf238b2d1cfb0222bef9d3b20d4c8f06c7a7ac", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "558a8a08ed78eb820efbfda1de196569d8bfa9b51e8371a1934fbb31345feda7"},
|
||||||
|
"ex_aws": {:hex, :ex_aws, "2.5.8", "0393cfbc5e4a9e7017845451a015d836a670397100aa4c86901980e2a2c5f7d4", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.3", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8f79777b7932168956c8cc3a6db41f5783aa816eb50de356aed3165a71e5f8c3"},
|
||||||
|
"ex_aws_s3": {:hex, :ex_aws_s3, "2.5.6", "d135983bbd8b6df6350dfd83999437725527c1bea151e5055760bfc9b2d17c20", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "9874e12847e469ca2f13a5689be04e546c16f63caf6380870b7f25bf7cb98875"},
|
||||||
"expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
|
"expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
|
||||||
|
"ffmpex": {:hex, :ffmpex, "0.11.0", "70d2e211a70e1d8cc1a81d73208d5efedda59d82db4c91160c79e5461529d291", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:rambo, "~> 0.3.0", [hex: :rambo, repo: "hexpm", optional: false]}], "hexpm", "2429d67badc91957ace572b9169615619740904a58791289ba54d99e57a164eb"},
|
||||||
"file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
|
"file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
|
||||||
"finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
|
"finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
|
||||||
"floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"},
|
"floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"},
|
||||||
|
@ -46,9 +49,11 @@
|
||||||
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
|
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
|
||||||
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
|
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
|
||||||
"postgrex": {:hex, :postgrex, "0.19.3", "a0bda6e3bc75ec07fca5b0a89bffd242ca209a4822a9533e7d3e84ee80707e19", [:mix], [{: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", "d31c28053655b78f47f948c85bb1cf86a9c1f8ead346ba1aa0d0df017fa05b61"},
|
"postgrex": {:hex, :postgrex, "0.19.3", "a0bda6e3bc75ec07fca5b0a89bffd242ca209a4822a9533e7d3e84ee80707e19", [:mix], [{: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", "d31c28053655b78f47f948c85bb1cf86a9c1f8ead346ba1aa0d0df017fa05b61"},
|
||||||
|
"rambo": {:hex, :rambo, "0.3.4", "8962ac3bd1a633ee9d0e8b44373c7913e3ce3d875b4151dcd060886092d2dce7", [:mix], [], "hexpm", "0cc54ed089fbbc84b65f4b8a774224ebfe60e5c80186fafc7910b3e379ad58f1"},
|
||||||
"redirect": {:hex, :redirect, "0.4.0", "98b46053504ee517bc3ad2fd04c064b64b48d339e1e18266355b30c4f8bb52b0", [:mix], [{:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.3 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "dfa29a8ecbad066ed0b73b34611cf24c78101719737f37bdf750f39197d67b97"},
|
"redirect": {:hex, :redirect, "0.4.0", "98b46053504ee517bc3ad2fd04c064b64b48d339e1e18266355b30c4f8bb52b0", [:mix], [{:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.3 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "dfa29a8ecbad066ed0b73b34611cf24c78101719737f37bdf750f39197d67b97"},
|
||||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
||||||
"superstreamer_player": {:git, "https://github.com/superstreamerapp/superstreamer.git", "9e868acede851f396b3db98fb9799ab4bf712b02", [sparse: "packages/player"]},
|
"superstreamer_player": {:git, "https://github.com/superstreamerapp/superstreamer.git", "9e868acede851f396b3db98fb9799ab4bf712b02", [sparse: "packages/player"]},
|
||||||
|
"sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
|
||||||
"swoosh": {:hex, :swoosh, "1.17.5", "14910d267a2633d4335917b37846e376e2067815601592629366c39845dad145", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {: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]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "629113d477bc82c4c3bffd15a25e8becc1c7ccc0f0e67743b017caddebb06f04"},
|
"swoosh": {:hex, :swoosh, "1.17.5", "14910d267a2633d4335917b37846e376e2067815601592629366c39845dad145", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {: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]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "629113d477bc82c4c3bffd15a25e8becc1c7ccc0f0e67743b017caddebb06f04"},
|
||||||
"tailwind": {:hex, :tailwind, "0.2.4", "5706ec47182d4e7045901302bf3a333e80f3d1af65c442ba9a9eed152fb26c2e", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c6e4a82b8727bab593700c998a4d98cf3d8025678bfde059aed71d0000c3e463"},
|
"tailwind": {:hex, :tailwind, "0.2.4", "5706ec47182d4e7045901302bf3a333e80f3d1af65c442ba9a9eed152fb26c2e", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c6e4a82b8727bab593700c998a4d98cf3d8025678bfde059aed71d0000c3e463"},
|
||||||
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
|
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
|
||||||
|
|
|
@ -3,7 +3,7 @@ defmodule Bright.Repo.Migrations.AddPlatformToUser do
|
||||||
|
|
||||||
def change do
|
def change do
|
||||||
alter table(:users) do
|
alter table(:users) do
|
||||||
add :is_admin, :boolean, default: false, null: false
|
add :platform, :string, null: false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
defmodule Bright.Repo.Migrations.RemoveEmailFromUser do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:users) do
|
||||||
|
remove :email
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,9 @@
|
||||||
|
defmodule Bright.Repo.Migrations.RemovePasswordFromUser do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:users) do
|
||||||
|
remove :hashed_password
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,9 @@
|
||||||
|
defmodule Bright.Repo.Migrations.RemovePlatformFromUser do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:users) do
|
||||||
|
remove :platform
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,9 @@
|
||||||
|
defmodule Bright.Repo.Migrations.AddThumbnailUrl do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:vods) do
|
||||||
|
add :thumbnail_url, :string
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,508 +0,0 @@
|
||||||
defmodule Bright.AccountsTest do
|
|
||||||
use Bright.DataCase
|
|
||||||
|
|
||||||
alias Bright.Accounts
|
|
||||||
|
|
||||||
import Bright.AccountsFixtures
|
|
||||||
alias Bright.Accounts.{User, UserToken}
|
|
||||||
|
|
||||||
describe "get_user_by_email/1" do
|
|
||||||
test "does not return the user if the email does not exist" do
|
|
||||||
refute Accounts.get_user_by_email("unknown@example.com")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns the user if the email exists" do
|
|
||||||
%{id: id} = user = user_fixture()
|
|
||||||
assert %User{id: ^id} = Accounts.get_user_by_email(user.email)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "get_user_by_email_and_password/2" do
|
|
||||||
test "does not return the user if the email does not exist" do
|
|
||||||
refute Accounts.get_user_by_email_and_password("unknown@example.com", "hello world!")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not return the user if the password is not valid" do
|
|
||||||
user = user_fixture()
|
|
||||||
refute Accounts.get_user_by_email_and_password(user.email, "invalid")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns the user if the email and password are valid" do
|
|
||||||
%{id: id} = user = user_fixture()
|
|
||||||
|
|
||||||
assert %User{id: ^id} =
|
|
||||||
Accounts.get_user_by_email_and_password(user.email, valid_user_password())
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "get_user!/1" do
|
|
||||||
test "raises if id is invalid" do
|
|
||||||
assert_raise Ecto.NoResultsError, fn ->
|
|
||||||
Accounts.get_user!(-1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns the user with the given id" do
|
|
||||||
%{id: id} = user = user_fixture()
|
|
||||||
assert %User{id: ^id} = Accounts.get_user!(user.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "register_user/1" do
|
|
||||||
test "requires email and password to be set" do
|
|
||||||
{:error, changeset} = Accounts.register_user(%{})
|
|
||||||
|
|
||||||
assert %{
|
|
||||||
password: ["can't be blank"],
|
|
||||||
email: ["can't be blank"]
|
|
||||||
} = errors_on(changeset)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "validates email and password when given" do
|
|
||||||
{:error, changeset} = Accounts.register_user(%{email: "not valid", password: "not valid"})
|
|
||||||
|
|
||||||
assert %{
|
|
||||||
email: ["must have the @ sign and no spaces"],
|
|
||||||
password: ["should be at least 12 character(s)"]
|
|
||||||
} = errors_on(changeset)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "validates maximum values for email and password for security" do
|
|
||||||
too_long = String.duplicate("db", 100)
|
|
||||||
{:error, changeset} = Accounts.register_user(%{email: too_long, password: too_long})
|
|
||||||
assert "should be at most 160 character(s)" in errors_on(changeset).email
|
|
||||||
assert "should be at most 72 character(s)" in errors_on(changeset).password
|
|
||||||
end
|
|
||||||
|
|
||||||
test "validates email uniqueness" do
|
|
||||||
%{email: email} = user_fixture()
|
|
||||||
{:error, changeset} = Accounts.register_user(%{email: email})
|
|
||||||
assert "has already been taken" in errors_on(changeset).email
|
|
||||||
|
|
||||||
# Now try with the upper cased email too, to check that email case is ignored.
|
|
||||||
{:error, changeset} = Accounts.register_user(%{email: String.upcase(email)})
|
|
||||||
assert "has already been taken" in errors_on(changeset).email
|
|
||||||
end
|
|
||||||
|
|
||||||
test "registers users with a hashed password" do
|
|
||||||
email = unique_user_email()
|
|
||||||
{:ok, user} = Accounts.register_user(valid_user_attributes(email: email))
|
|
||||||
assert user.email == email
|
|
||||||
assert is_binary(user.hashed_password)
|
|
||||||
assert is_nil(user.confirmed_at)
|
|
||||||
assert is_nil(user.password)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "change_user_registration/2" do
|
|
||||||
test "returns a changeset" do
|
|
||||||
assert %Ecto.Changeset{} = changeset = Accounts.change_user_registration(%User{})
|
|
||||||
assert changeset.required == [:password, :email]
|
|
||||||
end
|
|
||||||
|
|
||||||
test "allows fields to be set" do
|
|
||||||
email = unique_user_email()
|
|
||||||
password = valid_user_password()
|
|
||||||
|
|
||||||
changeset =
|
|
||||||
Accounts.change_user_registration(
|
|
||||||
%User{},
|
|
||||||
valid_user_attributes(email: email, password: password)
|
|
||||||
)
|
|
||||||
|
|
||||||
assert changeset.valid?
|
|
||||||
assert get_change(changeset, :email) == email
|
|
||||||
assert get_change(changeset, :password) == password
|
|
||||||
assert is_nil(get_change(changeset, :hashed_password))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "change_user_email/2" do
|
|
||||||
test "returns a user changeset" do
|
|
||||||
assert %Ecto.Changeset{} = changeset = Accounts.change_user_email(%User{})
|
|
||||||
assert changeset.required == [:email]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "apply_user_email/3" do
|
|
||||||
setup do
|
|
||||||
%{user: user_fixture()}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "requires email to change", %{user: user} do
|
|
||||||
{:error, changeset} = Accounts.apply_user_email(user, valid_user_password(), %{})
|
|
||||||
assert %{email: ["did not change"]} = errors_on(changeset)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "validates email", %{user: user} do
|
|
||||||
{:error, changeset} =
|
|
||||||
Accounts.apply_user_email(user, valid_user_password(), %{email: "not valid"})
|
|
||||||
|
|
||||||
assert %{email: ["must have the @ sign and no spaces"]} = errors_on(changeset)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "validates maximum value for email for security", %{user: user} do
|
|
||||||
too_long = String.duplicate("db", 100)
|
|
||||||
|
|
||||||
{:error, changeset} =
|
|
||||||
Accounts.apply_user_email(user, valid_user_password(), %{email: too_long})
|
|
||||||
|
|
||||||
assert "should be at most 160 character(s)" in errors_on(changeset).email
|
|
||||||
end
|
|
||||||
|
|
||||||
test "validates email uniqueness", %{user: user} do
|
|
||||||
%{email: email} = user_fixture()
|
|
||||||
password = valid_user_password()
|
|
||||||
|
|
||||||
{:error, changeset} = Accounts.apply_user_email(user, password, %{email: email})
|
|
||||||
|
|
||||||
assert "has already been taken" in errors_on(changeset).email
|
|
||||||
end
|
|
||||||
|
|
||||||
test "validates current password", %{user: user} do
|
|
||||||
{:error, changeset} =
|
|
||||||
Accounts.apply_user_email(user, "invalid", %{email: unique_user_email()})
|
|
||||||
|
|
||||||
assert %{current_password: ["is not valid"]} = errors_on(changeset)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "applies the email without persisting it", %{user: user} do
|
|
||||||
email = unique_user_email()
|
|
||||||
{:ok, user} = Accounts.apply_user_email(user, valid_user_password(), %{email: email})
|
|
||||||
assert user.email == email
|
|
||||||
assert Accounts.get_user!(user.id).email != email
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "deliver_user_update_email_instructions/3" do
|
|
||||||
setup do
|
|
||||||
%{user: user_fixture()}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "sends token through notification", %{user: user} do
|
|
||||||
token =
|
|
||||||
extract_user_token(fn url ->
|
|
||||||
Accounts.deliver_user_update_email_instructions(user, "current@example.com", url)
|
|
||||||
end)
|
|
||||||
|
|
||||||
{:ok, token} = Base.url_decode64(token, padding: false)
|
|
||||||
assert user_token = Repo.get_by(UserToken, token: :crypto.hash(:sha256, token))
|
|
||||||
assert user_token.user_id == user.id
|
|
||||||
assert user_token.sent_to == user.email
|
|
||||||
assert user_token.context == "change:current@example.com"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "update_user_email/2" do
|
|
||||||
setup do
|
|
||||||
user = user_fixture()
|
|
||||||
email = unique_user_email()
|
|
||||||
|
|
||||||
token =
|
|
||||||
extract_user_token(fn url ->
|
|
||||||
Accounts.deliver_user_update_email_instructions(%{user | email: email}, user.email, url)
|
|
||||||
end)
|
|
||||||
|
|
||||||
%{user: user, token: token, email: email}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "updates the email with a valid token", %{user: user, token: token, email: email} do
|
|
||||||
assert Accounts.update_user_email(user, token) == :ok
|
|
||||||
changed_user = Repo.get!(User, user.id)
|
|
||||||
assert changed_user.email != user.email
|
|
||||||
assert changed_user.email == email
|
|
||||||
assert changed_user.confirmed_at
|
|
||||||
assert changed_user.confirmed_at != user.confirmed_at
|
|
||||||
refute Repo.get_by(UserToken, user_id: user.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not update email with invalid token", %{user: user} do
|
|
||||||
assert Accounts.update_user_email(user, "oops") == :error
|
|
||||||
assert Repo.get!(User, user.id).email == user.email
|
|
||||||
assert Repo.get_by(UserToken, user_id: user.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not update email if user email changed", %{user: user, token: token} do
|
|
||||||
assert Accounts.update_user_email(%{user | email: "current@example.com"}, token) == :error
|
|
||||||
assert Repo.get!(User, user.id).email == user.email
|
|
||||||
assert Repo.get_by(UserToken, user_id: user.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not update email if token expired", %{user: user, token: token} do
|
|
||||||
{1, nil} = Repo.update_all(UserToken, set: [inserted_at: ~N[2020-01-01 00:00:00]])
|
|
||||||
assert Accounts.update_user_email(user, token) == :error
|
|
||||||
assert Repo.get!(User, user.id).email == user.email
|
|
||||||
assert Repo.get_by(UserToken, user_id: user.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "change_user_password/2" do
|
|
||||||
test "returns a user changeset" do
|
|
||||||
assert %Ecto.Changeset{} = changeset = Accounts.change_user_password(%User{})
|
|
||||||
assert changeset.required == [:password]
|
|
||||||
end
|
|
||||||
|
|
||||||
test "allows fields to be set" do
|
|
||||||
changeset =
|
|
||||||
Accounts.change_user_password(%User{}, %{
|
|
||||||
"password" => "new valid password"
|
|
||||||
})
|
|
||||||
|
|
||||||
assert changeset.valid?
|
|
||||||
assert get_change(changeset, :password) == "new valid password"
|
|
||||||
assert is_nil(get_change(changeset, :hashed_password))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "update_user_password/3" do
|
|
||||||
setup do
|
|
||||||
%{user: user_fixture()}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "validates password", %{user: user} do
|
|
||||||
{:error, changeset} =
|
|
||||||
Accounts.update_user_password(user, valid_user_password(), %{
|
|
||||||
password: "not valid",
|
|
||||||
password_confirmation: "another"
|
|
||||||
})
|
|
||||||
|
|
||||||
assert %{
|
|
||||||
password: ["should be at least 12 character(s)"],
|
|
||||||
password_confirmation: ["does not match password"]
|
|
||||||
} = errors_on(changeset)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "validates maximum values for password for security", %{user: user} do
|
|
||||||
too_long = String.duplicate("db", 100)
|
|
||||||
|
|
||||||
{:error, changeset} =
|
|
||||||
Accounts.update_user_password(user, valid_user_password(), %{password: too_long})
|
|
||||||
|
|
||||||
assert "should be at most 72 character(s)" in errors_on(changeset).password
|
|
||||||
end
|
|
||||||
|
|
||||||
test "validates current password", %{user: user} do
|
|
||||||
{:error, changeset} =
|
|
||||||
Accounts.update_user_password(user, "invalid", %{password: valid_user_password()})
|
|
||||||
|
|
||||||
assert %{current_password: ["is not valid"]} = errors_on(changeset)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "updates the password", %{user: user} do
|
|
||||||
{:ok, user} =
|
|
||||||
Accounts.update_user_password(user, valid_user_password(), %{
|
|
||||||
password: "new valid password"
|
|
||||||
})
|
|
||||||
|
|
||||||
assert is_nil(user.password)
|
|
||||||
assert Accounts.get_user_by_email_and_password(user.email, "new valid password")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "deletes all tokens for the given user", %{user: user} do
|
|
||||||
_ = Accounts.generate_user_session_token(user)
|
|
||||||
|
|
||||||
{:ok, _} =
|
|
||||||
Accounts.update_user_password(user, valid_user_password(), %{
|
|
||||||
password: "new valid password"
|
|
||||||
})
|
|
||||||
|
|
||||||
refute Repo.get_by(UserToken, user_id: user.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "generate_user_session_token/1" do
|
|
||||||
setup do
|
|
||||||
%{user: user_fixture()}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "generates a token", %{user: user} do
|
|
||||||
token = Accounts.generate_user_session_token(user)
|
|
||||||
assert user_token = Repo.get_by(UserToken, token: token)
|
|
||||||
assert user_token.context == "session"
|
|
||||||
|
|
||||||
# Creating the same token for another user should fail
|
|
||||||
assert_raise Ecto.ConstraintError, fn ->
|
|
||||||
Repo.insert!(%UserToken{
|
|
||||||
token: user_token.token,
|
|
||||||
user_id: user_fixture().id,
|
|
||||||
context: "session"
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "get_user_by_session_token/1" do
|
|
||||||
setup do
|
|
||||||
user = user_fixture()
|
|
||||||
token = Accounts.generate_user_session_token(user)
|
|
||||||
%{user: user, token: token}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns user by token", %{user: user, token: token} do
|
|
||||||
assert session_user = Accounts.get_user_by_session_token(token)
|
|
||||||
assert session_user.id == user.id
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not return user for invalid token" do
|
|
||||||
refute Accounts.get_user_by_session_token("oops")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not return user for expired token", %{token: token} do
|
|
||||||
{1, nil} = Repo.update_all(UserToken, set: [inserted_at: ~N[2020-01-01 00:00:00]])
|
|
||||||
refute Accounts.get_user_by_session_token(token)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "delete_user_session_token/1" do
|
|
||||||
test "deletes the token" do
|
|
||||||
user = user_fixture()
|
|
||||||
token = Accounts.generate_user_session_token(user)
|
|
||||||
assert Accounts.delete_user_session_token(token) == :ok
|
|
||||||
refute Accounts.get_user_by_session_token(token)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "deliver_user_confirmation_instructions/2" do
|
|
||||||
setup do
|
|
||||||
%{user: user_fixture()}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "sends token through notification", %{user: user} do
|
|
||||||
token =
|
|
||||||
extract_user_token(fn url ->
|
|
||||||
Accounts.deliver_user_confirmation_instructions(user, url)
|
|
||||||
end)
|
|
||||||
|
|
||||||
{:ok, token} = Base.url_decode64(token, padding: false)
|
|
||||||
assert user_token = Repo.get_by(UserToken, token: :crypto.hash(:sha256, token))
|
|
||||||
assert user_token.user_id == user.id
|
|
||||||
assert user_token.sent_to == user.email
|
|
||||||
assert user_token.context == "confirm"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "confirm_user/1" do
|
|
||||||
setup do
|
|
||||||
user = user_fixture()
|
|
||||||
|
|
||||||
token =
|
|
||||||
extract_user_token(fn url ->
|
|
||||||
Accounts.deliver_user_confirmation_instructions(user, url)
|
|
||||||
end)
|
|
||||||
|
|
||||||
%{user: user, token: token}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "confirms the email with a valid token", %{user: user, token: token} do
|
|
||||||
assert {:ok, confirmed_user} = Accounts.confirm_user(token)
|
|
||||||
assert confirmed_user.confirmed_at
|
|
||||||
assert confirmed_user.confirmed_at != user.confirmed_at
|
|
||||||
assert Repo.get!(User, user.id).confirmed_at
|
|
||||||
refute Repo.get_by(UserToken, user_id: user.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not confirm with invalid token", %{user: user} do
|
|
||||||
assert Accounts.confirm_user("oops") == :error
|
|
||||||
refute Repo.get!(User, user.id).confirmed_at
|
|
||||||
assert Repo.get_by(UserToken, user_id: user.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not confirm email if token expired", %{user: user, token: token} do
|
|
||||||
{1, nil} = Repo.update_all(UserToken, set: [inserted_at: ~N[2020-01-01 00:00:00]])
|
|
||||||
assert Accounts.confirm_user(token) == :error
|
|
||||||
refute Repo.get!(User, user.id).confirmed_at
|
|
||||||
assert Repo.get_by(UserToken, user_id: user.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "deliver_user_reset_password_instructions/2" do
|
|
||||||
setup do
|
|
||||||
%{user: user_fixture()}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "sends token through notification", %{user: user} do
|
|
||||||
token =
|
|
||||||
extract_user_token(fn url ->
|
|
||||||
Accounts.deliver_user_reset_password_instructions(user, url)
|
|
||||||
end)
|
|
||||||
|
|
||||||
{:ok, token} = Base.url_decode64(token, padding: false)
|
|
||||||
assert user_token = Repo.get_by(UserToken, token: :crypto.hash(:sha256, token))
|
|
||||||
assert user_token.user_id == user.id
|
|
||||||
assert user_token.sent_to == user.email
|
|
||||||
assert user_token.context == "reset_password"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "get_user_by_reset_password_token/1" do
|
|
||||||
setup do
|
|
||||||
user = user_fixture()
|
|
||||||
|
|
||||||
token =
|
|
||||||
extract_user_token(fn url ->
|
|
||||||
Accounts.deliver_user_reset_password_instructions(user, url)
|
|
||||||
end)
|
|
||||||
|
|
||||||
%{user: user, token: token}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns the user with valid token", %{user: %{id: id}, token: token} do
|
|
||||||
assert %User{id: ^id} = Accounts.get_user_by_reset_password_token(token)
|
|
||||||
assert Repo.get_by(UserToken, user_id: id)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not return the user with invalid token", %{user: user} do
|
|
||||||
refute Accounts.get_user_by_reset_password_token("oops")
|
|
||||||
assert Repo.get_by(UserToken, user_id: user.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not return the user if token expired", %{user: user, token: token} do
|
|
||||||
{1, nil} = Repo.update_all(UserToken, set: [inserted_at: ~N[2020-01-01 00:00:00]])
|
|
||||||
refute Accounts.get_user_by_reset_password_token(token)
|
|
||||||
assert Repo.get_by(UserToken, user_id: user.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "reset_user_password/2" do
|
|
||||||
setup do
|
|
||||||
%{user: user_fixture()}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "validates password", %{user: user} do
|
|
||||||
{:error, changeset} =
|
|
||||||
Accounts.reset_user_password(user, %{
|
|
||||||
password: "not valid",
|
|
||||||
password_confirmation: "another"
|
|
||||||
})
|
|
||||||
|
|
||||||
assert %{
|
|
||||||
password: ["should be at least 12 character(s)"],
|
|
||||||
password_confirmation: ["does not match password"]
|
|
||||||
} = errors_on(changeset)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "validates maximum values for password for security", %{user: user} do
|
|
||||||
too_long = String.duplicate("db", 100)
|
|
||||||
{:error, changeset} = Accounts.reset_user_password(user, %{password: too_long})
|
|
||||||
assert "should be at most 72 character(s)" in errors_on(changeset).password
|
|
||||||
end
|
|
||||||
|
|
||||||
test "updates the password", %{user: user} do
|
|
||||||
{:ok, updated_user} = Accounts.reset_user_password(user, %{password: "new valid password"})
|
|
||||||
assert is_nil(updated_user.password)
|
|
||||||
assert Accounts.get_user_by_email_and_password(user.email, "new valid password")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "deletes all tokens for the given user", %{user: user} do
|
|
||||||
_ = Accounts.generate_user_session_token(user)
|
|
||||||
{:ok, _} = Accounts.reset_user_password(user, %{password: "new valid password"})
|
|
||||||
refute Repo.get_by(UserToken, user_id: user.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "inspect/2 for the User module" do
|
|
||||||
test "does not include password" do
|
|
||||||
refute inspect(%User{password: "123456"}) =~ "password: \"123456\""
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
defmodule Bright.B2Test do
|
||||||
|
use Bright.DataCase
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
describe "B2" do
|
||||||
|
|
||||||
|
alias Bright.B2
|
||||||
|
|
||||||
|
|
||||||
|
@tag :acceptance
|
||||||
|
test "put/1" do
|
||||||
|
local_file = Path.absname("test/fixtures/SampleVideo_1280x720_1mb.mp4")
|
||||||
|
object_key = "test/SampleVideo_1280x720_1mb.mp4"
|
||||||
|
{:ok, remote_file} = B2.put(local_file, object_key)
|
||||||
|
assert Regex.match?(~r/SampleVideo/, remote_file)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
@tag :acceptance
|
||||||
|
test "get/2" do
|
||||||
|
local_file = "/tmp/SampleVideo_1280x720_1mb.mp4"
|
||||||
|
File.rm(local_file)
|
||||||
|
|
||||||
|
{:ok, filename} = B2.get("test/SampleVideo_1280x720_1mb.mp4", local_file)
|
||||||
|
assert File.exists?(local_file)
|
||||||
|
assert filename === local_file
|
||||||
|
|
||||||
|
{:ok, stat} = File.stat(local_file)
|
||||||
|
assert stat.size > 0, "File is empty"
|
||||||
|
|
||||||
|
File.rm!(local_file)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -0,0 +1,26 @@
|
||||||
|
defmodule Bright.CacheTest do
|
||||||
|
use Bright.DataCase
|
||||||
|
|
||||||
|
alias Bright.Cache
|
||||||
|
|
||||||
|
describe "cache" do
|
||||||
|
|
||||||
|
@tag :unit
|
||||||
|
test "generate_filename generates a random alphanumeric prefix with the correct basename and extension" do
|
||||||
|
# Test with a URL
|
||||||
|
url = "https://example.com/my_video.mp4"
|
||||||
|
filename = Cache.generate_filename(url)
|
||||||
|
|
||||||
|
assert Regex.match?(~r/^[a-zA-Z0-9]+-my_video\.mp4$/, filename)
|
||||||
|
|
||||||
|
# Test with a file path
|
||||||
|
path = "/home/cj/Downloads/taco.mp4"
|
||||||
|
filename = Cache.generate_filename(path)
|
||||||
|
|
||||||
|
assert Regex.match?(~r/^[a-zA-Z0-9]+-taco\.mp4$/, filename)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,67 @@
|
||||||
|
defmodule Bright.ImagesTest do
|
||||||
|
use Bright.DataCase
|
||||||
|
|
||||||
|
alias Bright.Images
|
||||||
|
|
||||||
|
@test_fixture "./test/fixtures/SampleVideo_1280x720_1mb.mp4"
|
||||||
|
|
||||||
|
describe "thumbnails" do
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
test "should generate a 5x5 thumbnail using a local file" do
|
||||||
|
# ffmpeg -y -i ~/Videos/moose-encounter_75.mp4 -frames:v 1 -vf 'select=not(mod(n\,257)),scale=160:-1,tile=5x5' -update 1 -fps_mode passthrough ~/Videos/thumb.jpg
|
||||||
|
|
||||||
|
basename = "thumb.jpg"
|
||||||
|
random_string = for _ <- 1..12, into: "", do: <<Enum.random(~c"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")>>
|
||||||
|
output_file = "/tmp/#{random_string}-#{basename}"
|
||||||
|
IO.puts "output_file=#{inspect(output_file)} @test_fixture=#{inspect(@test_fixture)}"
|
||||||
|
|
||||||
|
Images.create_thumbnail(@test_fixture, output_file)
|
||||||
|
|
||||||
|
assert File.exists?(output_file)
|
||||||
|
{:ok, stat} = File.stat(output_file)
|
||||||
|
assert stat.size > 0, "File is empty"
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
# Feature creep! Download the image for now. Make it work, first. THen make it right. THEN make it fast.
|
||||||
|
# test "should generate a 5x5 thumbnail using a remote file" do
|
||||||
|
# basename = "thumb.jpg"
|
||||||
|
# random_string = for _ <- 1..12, into: "", do: <<Enum.random(~c"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")>>
|
||||||
|
# output_file = "/tmp/#{random_string}-#{basename}"
|
||||||
|
# input_url = "http://38.242.193.246:8081/fixtures/2024-12-19T03-14-03Z.ts"
|
||||||
|
# {:ok, output} = Images.create_thumbnail(input_url, output_file)
|
||||||
|
# assert Regex.match?("/tmp", output)
|
||||||
|
# end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
describe "get_video_duration" do
|
||||||
|
@tag :integration
|
||||||
|
test "should get video stream duration" do
|
||||||
|
{:ok, duration} = Images.get_video_duration(@test_fixture)
|
||||||
|
assert duration === "5.280000"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "get_video_framecount" do
|
||||||
|
@tag :integration
|
||||||
|
test "should get video frame count" do
|
||||||
|
{:ok, nb_frames} = Images.get_video_framecount(@test_fixture)
|
||||||
|
assert nb_frames === 132
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
end
|
|
@ -0,0 +1,130 @@
|
||||||
|
|
||||||
|
defmodule Bright.CreateHlsPlaylistTest do
|
||||||
|
use Bright.DataCase
|
||||||
|
use Oban.Testing, repo: Bright.Repo
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
|
||||||
|
alias Bright.ObanWorkers.{ProcessVod, CreateHlsPlaylist}
|
||||||
|
alias Bright.Streams
|
||||||
|
alias Bright.Streams.Stream
|
||||||
|
|
||||||
|
|
||||||
|
describe "CreateHlsPlaylist" do
|
||||||
|
|
||||||
|
@tag :integration
|
||||||
|
test "sheduling upon vod creation" do
|
||||||
|
|
||||||
|
example_video = "http://example.com/video.ts"
|
||||||
|
stream_attrs = %{date: ~U[2024-12-28 03:31:00Z], title: "some title", notes: "some notes"}
|
||||||
|
{:ok, %Stream{} = stream} = Streams.create_stream(stream_attrs)
|
||||||
|
{:ok, _vod} = Streams.create_vod(%{stream_id: stream.id, origin_temp_input_url: example_video})
|
||||||
|
|
||||||
|
|
||||||
|
assert_enqueued worker: ProcessVod, queue: :default
|
||||||
|
assert %{success: 1} = Oban.drain_queue(queue: :default) # ProcessVod is what queues CreateThumbnail so we need to make it run
|
||||||
|
|
||||||
|
assert_enqueued [worker: CreateHlsPlaylist, queue: :default], 1000
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
@tag :integration
|
||||||
|
test "not scheduled when origin_temp_input_url is missing" do
|
||||||
|
|
||||||
|
stream_attrs = %{date: ~U[2024-12-28 03:31:00Z], title: "some title", notes: "some notes"}
|
||||||
|
{:ok, %Stream{} = stream} = Streams.create_stream(stream_attrs)
|
||||||
|
{:ok, _vod} = Streams.create_vod(%{stream_id: stream.id})
|
||||||
|
|
||||||
|
refute_enqueued worker: CreateHlsPlaylist
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
describe "perform/1" do
|
||||||
|
# test "throw when vod_id is missing" do
|
||||||
|
# job_args = %{"vod_id" => 1}
|
||||||
|
# {:error, reason} = CreateHlsPlaylist.perform(%Oban.Job{args: job_args})
|
||||||
|
# assert Regex.match?(~r/missing.*vod_id/i, reason)
|
||||||
|
# end
|
||||||
|
|
||||||
|
# test "evergreen?" do
|
||||||
|
# job_args = %{"vod_id" => 1, "input_url" => "https://example.com/my_video.mp4"}
|
||||||
|
# assert :ok = CreateHlsPlaylist.perform(%Oban.Job{args: job_args})
|
||||||
|
# end
|
||||||
|
|
||||||
|
# test "bypassing activation when sign up fails" do
|
||||||
|
# {:error, _reason} = CreateHlsPlaylist.perform(%Oban.Job{args: job_args})
|
||||||
|
|
||||||
|
# refute_enqueued worker: MyApp.ActivationWorker
|
||||||
|
# end
|
||||||
|
|
||||||
|
# test "successfully creates an HLS playlist and updates the VOD" do
|
||||||
|
# # Mock the VOD to be returned by Repo.get!/2
|
||||||
|
# vod = %Vod{id: 1, playlist_url: nil}
|
||||||
|
# expect(RepoMock, :get!, fn Vod, 1 -> vod end)
|
||||||
|
|
||||||
|
# # Mock the start_transcode function
|
||||||
|
# expect(HTTPoisonMock, :post, fn url, body, headers ->
|
||||||
|
# assert url == "#{System.get_env("SUPERSTREAMER_URL")}/transcode"
|
||||||
|
# assert headers == [
|
||||||
|
# {"authorization", "Bearer #{System.get_env("SUPERSTREAMER_AUTH_TOKEN")}"},
|
||||||
|
# {"content-type", "application/json"}
|
||||||
|
# ]
|
||||||
|
# {:ok, %HTTPoison.Response{status_code: 200, body: ~s({"jobId": "transcode-job-id"})}}
|
||||||
|
# end)
|
||||||
|
|
||||||
|
# # Mock the polling of the transcode job
|
||||||
|
# expect(HTTPoisonMock, :get, fn url, headers ->
|
||||||
|
# assert url == "#{System.get_env("SUPERSTREAMER_URL")}/jobs/transcode-job-id"
|
||||||
|
# {:ok, %HTTPoison.Response{
|
||||||
|
# status_code: 200,
|
||||||
|
# body: ~s({"state": "completed", "outputData": {"assetId": "asset-id"}})
|
||||||
|
# }}
|
||||||
|
# end)
|
||||||
|
|
||||||
|
# # Mock the start_package function
|
||||||
|
# expect(HTTPoisonMock, :post, fn url, body, headers ->
|
||||||
|
# assert url == "#{System.get_env("SUPERSTREAMER_URL")}/package"
|
||||||
|
# {:ok, %HTTPoison.Response{status_code: 200, body: ~s({"jobId": "package-job-id"})}}
|
||||||
|
# end)
|
||||||
|
|
||||||
|
# # Mock the polling of the package job
|
||||||
|
# expect(HTTPoisonMock, :get, fn url, headers ->
|
||||||
|
# assert url == "#{System.get_env("SUPERSTREAMER_URL")}/jobs/package-job-id"
|
||||||
|
# {:ok, %HTTPoison.Response{
|
||||||
|
# status_code: 200,
|
||||||
|
# body: ~s({"state": "completed", "outputData": {"assetId": "asset-id"}})
|
||||||
|
# }}
|
||||||
|
# end)
|
||||||
|
|
||||||
|
|
||||||
|
# # Mock the Repo.update!/1 call
|
||||||
|
# expect(RepoMock, :update!, fn changeset ->
|
||||||
|
# assert changeset.changes.playlist_url == "#{System.get_env("PUBLIC_S3_ENDPOINT")}/package/asset-id/hls/master.m3u8"
|
||||||
|
# {:ok, changeset}
|
||||||
|
# end)
|
||||||
|
|
||||||
|
# # Call the worker's perform function
|
||||||
|
# job_args = %{"vod_id" => 1, "input_url" => "https://example.com/input.mp4"}
|
||||||
|
# assert :ok = CreateHlsPlaylist.perform(%Oban.Job{args: job_args})
|
||||||
|
# end
|
||||||
|
|
||||||
|
# test "handles errors gracefully" do
|
||||||
|
# # Mock the VOD to be returned by Repo.get!/2
|
||||||
|
# vod = %Vod{id: 1, playlist_url: nil}
|
||||||
|
# expect(RepoMock, :get!, fn Vod, 1 -> vod end)
|
||||||
|
|
||||||
|
# # Mock the start_transcode function to return an error
|
||||||
|
# expect(HTTPoisonMock, :post, fn _url, _body, _headers ->
|
||||||
|
# {:error, %HTTPoison.Error{reason: "transcoding failed"}}
|
||||||
|
# end)
|
||||||
|
|
||||||
|
# # Call the worker's perform function
|
||||||
|
# job_args = %{"vod_id" => 1, "input_url" => "https://example.com/input.mp4"}
|
||||||
|
# assert {:error, _reason} = CreateHlsPlaylist.perform(%Oban.Job{args: job_args})
|
||||||
|
# end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,31 @@
|
||||||
|
defmodule Bright.ObanWorkers.CreateS3AssetTest do
|
||||||
|
use Bright.DataCase
|
||||||
|
use Oban.Testing, repo: Bright.Repo
|
||||||
|
alias Bright.ObanWorkers.CreateS3Asset
|
||||||
|
|
||||||
|
@tag :unit
|
||||||
|
test "creating a new s3 asset (unit test)" do
|
||||||
|
|
||||||
|
url = "https://example.com/video.ts"
|
||||||
|
key = "video.ts"
|
||||||
|
{:ok, asset} = perform_job(CreateS3Asset, %{url: url, key: key})
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag :acceptance
|
||||||
|
test "creating a new s3 asset (acceptance test)" do
|
||||||
|
|
||||||
|
|
||||||
|
url = "http://38.242.193.246:8081/fixtures/2024-12-19T03-10-30Z.ts"
|
||||||
|
key = unique_filename
|
||||||
|
|
||||||
|
example_video = "http://example.com/video.ts"
|
||||||
|
stream_attrs = %{date: ~U[2024-12-28 03:31:00Z], title: "some title", notes: "some notes"}
|
||||||
|
{:ok, %Stream{} = stream} = Streams.create_stream(stream_attrs)
|
||||||
|
{:ok, _vod} = Streams.create_vod(%{stream_id: stream.id, origin_temp_input_url: example_video})
|
||||||
|
|
||||||
|
|
||||||
|
perform_job(CreateS3Asset, %{url: url, key: key})
|
||||||
|
{:ok, asset} = Oban.drain_queue(queue: :default)
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,39 @@
|
||||||
|
defmodule Bright.ObanWorkers.CreateThumbnailTest do
|
||||||
|
use Bright.DataCase
|
||||||
|
use Oban.Testing, repo: Bright.Repo
|
||||||
|
alias Bright.ObanWorkers.CreateThumbnail
|
||||||
|
alias Bright.ObanWorkers.ProcessVod
|
||||||
|
alias Bright.Streams
|
||||||
|
alias Bright.Streams.Stream
|
||||||
|
|
||||||
|
describe "CreateThumbnail" do
|
||||||
|
|
||||||
|
@tag :integration
|
||||||
|
test "sheduling upon vod creation" do
|
||||||
|
|
||||||
|
example_video = "http://example.com/video.ts"
|
||||||
|
stream_attrs = %{date: ~U[2024-12-28 03:31:00Z], title: "some title", notes: "some notes"}
|
||||||
|
{:ok, %Stream{} = stream} = Streams.create_stream(stream_attrs)
|
||||||
|
{:ok, vod} = Streams.create_vod(%{stream_id: stream.id, origin_temp_input_url: example_video})
|
||||||
|
assert stream.notes === "some notes" # sanity check, making sure the stream gets created
|
||||||
|
assert vod.origin_temp_input_url === example_video
|
||||||
|
|
||||||
|
assert_enqueued worker: ProcessVod, queue: :default
|
||||||
|
Oban.drain_queue(queue: :default) # We need to run ProcessVod Job because that is what queues CreateThumbnail
|
||||||
|
|
||||||
|
assert_enqueued worker: CreateThumbnail, queue: :default
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
@tag :integration
|
||||||
|
test "not scheduled when origin_temp_input_url is missing" do
|
||||||
|
|
||||||
|
stream_attrs = %{date: ~U[2024-12-28 03:31:00Z], title: "some title", notes: "some notes"}
|
||||||
|
{:ok, %Stream{} = stream} = Streams.create_stream(stream_attrs)
|
||||||
|
{:ok, _vod} = Streams.create_vod(%{stream_id: stream.id})
|
||||||
|
|
||||||
|
refute_enqueued worker: CreateThumbnail
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,62 @@
|
||||||
|
defmodule Bright.UploadsTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
@test_url "https://futureporn-b2.b-cdn.net/projekt-melody.jpg"
|
||||||
|
@test_file_path "./test_download.txt"
|
||||||
|
@s3_bucket Application.compile_env(:ex_aws, [:s3, :bucket])
|
||||||
|
|
||||||
|
|
||||||
|
describe "download_file/2" do
|
||||||
|
@tag :acceptance
|
||||||
|
@tag timeout: :infinity
|
||||||
|
test "downloads a file successfully" do
|
||||||
|
# Ensure a valid file exists at the URL for testing purposes
|
||||||
|
assert {:ok, _res} = Bright.Uploads.download_file(@test_url, @test_file_path)
|
||||||
|
assert File.exists?(@test_file_path)
|
||||||
|
|
||||||
|
|
||||||
|
{:ok, stat} = File.stat(@test_file_path)
|
||||||
|
assert stat.size > 0, "File is empty"
|
||||||
|
|
||||||
|
File.rm!(@test_file_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns an error for an invalid URL" do
|
||||||
|
invalid_url = "https://nonexistent.url/file.txt"
|
||||||
|
|
||||||
|
assert {:error, _reason} = Bright.Uploads.download_file(invalid_url, @test_file_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "upload_file_to_s3/3" do
|
||||||
|
setup do
|
||||||
|
File.write!(@test_file_path, "This is a test file.")
|
||||||
|
on_exit(fn -> File.rm!(@test_file_path) end)
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag :acceptance
|
||||||
|
test "uploads a file to S3 successfully" do
|
||||||
|
basename = "random.txt"
|
||||||
|
random_string = for _ <- 1..12, into: "", do: <<Enum.random(~c"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")>>
|
||||||
|
unique_filename = "#{random_string}-#{basename}"
|
||||||
|
|
||||||
|
|
||||||
|
assert {:ok, %{status_code: 200}} = Bright.Uploads.upload_file_to_s3(@test_file_path, @s3_bucket, unique_filename)
|
||||||
|
|
||||||
|
# Optionally verify the file exists in S3 (requires an S3 client)
|
||||||
|
{:ok, %{status_code: 200}} =
|
||||||
|
ExAws.S3.head_object(@s3_bucket, unique_filename)
|
||||||
|
|> ExAws.request()
|
||||||
|
|
||||||
|
# Cleanup: Delete the uploaded file from S3
|
||||||
|
ExAws.S3.delete_object(@s3_bucket, unique_filename)
|
||||||
|
|> ExAws.request()
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag :acceptance
|
||||||
|
test "returns an error for a nonexistent file" do
|
||||||
|
assert {:error, :enoent} = Bright.Uploads.upload_file_to_s3("nonexistent.txt", @s3_bucket, "test/nonexistant.txt")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -52,9 +52,6 @@ defmodule Bright.VtubersTest do
|
||||||
assert vtuber.twitter_id == "some twitter_id"
|
assert vtuber.twitter_id == "some twitter_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "create_vtuber/1 with invalid data returns error changeset" do
|
|
||||||
assert {:error, %Ecto.Changeset{}} = Vtubers.create_vtuber(@invalid_attrs)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "update_vtuber/2 with valid data updates the vtuber" do
|
test "update_vtuber/2 with valid data updates the vtuber" do
|
||||||
vtuber = vtuber_fixture()
|
vtuber = vtuber_fixture()
|
||||||
|
@ -105,57 +102,12 @@ defmodule Bright.VtubersTest do
|
||||||
vtuber = vtuber_fixture()
|
vtuber = vtuber_fixture()
|
||||||
assert %Ecto.Changeset{} = Vtubers.change_vtuber(vtuber)
|
assert %Ecto.Changeset{} = Vtubers.change_vtuber(vtuber)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
describe "vtubers" do
|
|
||||||
alias Bright.Vtubers.Vtuber
|
|
||||||
|
|
||||||
import Bright.VtubersFixtures
|
|
||||||
|
|
||||||
@invalid_attrs %{}
|
|
||||||
|
|
||||||
test "list_vtubers/0 returns all vtubers" do
|
|
||||||
vtuber = vtuber_fixture()
|
|
||||||
assert Vtubers.list_vtubers() == [vtuber]
|
|
||||||
end
|
|
||||||
|
|
||||||
test "get_vtuber!/1 returns the vtuber with given id" do
|
|
||||||
vtuber = vtuber_fixture()
|
|
||||||
assert Vtubers.get_vtuber!(vtuber.id) == vtuber
|
|
||||||
end
|
|
||||||
|
|
||||||
test "create_vtuber/1 with valid data creates a vtuber" do
|
|
||||||
valid_attrs = %{}
|
|
||||||
|
|
||||||
assert {:ok, %Vtuber{} = vtuber} = Vtubers.create_vtuber(valid_attrs)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "create_vtuber/1 with invalid data returns error changeset" do
|
test "create_vtuber/1 with invalid data returns error changeset" do
|
||||||
assert {:error, %Ecto.Changeset{}} = Vtubers.create_vtuber(@invalid_attrs)
|
assert {:error, %Ecto.Changeset{}} = Vtubers.create_vtuber(@invalid_attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "update_vtuber/2 with valid data updates the vtuber" do
|
|
||||||
vtuber = vtuber_fixture()
|
|
||||||
update_attrs = %{}
|
|
||||||
|
|
||||||
assert {:ok, %Vtuber{} = vtuber} = Vtubers.update_vtuber(vtuber, update_attrs)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "update_vtuber/2 with invalid data returns error changeset" do
|
|
||||||
vtuber = vtuber_fixture()
|
|
||||||
assert {:error, %Ecto.Changeset{}} = Vtubers.update_vtuber(vtuber, @invalid_attrs)
|
|
||||||
assert vtuber == Vtubers.get_vtuber!(vtuber.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "delete_vtuber/1 deletes the vtuber" do
|
|
||||||
vtuber = vtuber_fixture()
|
|
||||||
assert {:ok, %Vtuber{}} = Vtubers.delete_vtuber(vtuber)
|
|
||||||
assert_raise Ecto.NoResultsError, fn -> Vtubers.get_vtuber!(vtuber.id) end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "change_vtuber/1 returns a vtuber changeset" do
|
|
||||||
vtuber = vtuber_fixture()
|
|
||||||
assert %Ecto.Changeset{} = Vtubers.change_vtuber(vtuber)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,84 +0,0 @@
|
||||||
defmodule BrightWeb.ProductControllerTest do
|
|
||||||
use BrightWeb.ConnCase
|
|
||||||
|
|
||||||
import Bright.CatalogFixtures
|
|
||||||
|
|
||||||
@create_attrs %{description: "some description", title: "some title", price: "120.5", views: 42}
|
|
||||||
@update_attrs %{description: "some updated description", title: "some updated title", price: "456.7", views: 43}
|
|
||||||
@invalid_attrs %{description: nil, title: nil, price: nil, views: nil}
|
|
||||||
|
|
||||||
describe "index" do
|
|
||||||
test "lists all products", %{conn: conn} do
|
|
||||||
conn = get(conn, ~p"/products")
|
|
||||||
assert html_response(conn, 200) =~ "Listing Products"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "new product" do
|
|
||||||
test "renders form", %{conn: conn} do
|
|
||||||
conn = get(conn, ~p"/products/new")
|
|
||||||
assert html_response(conn, 200) =~ "New Product"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "create product" do
|
|
||||||
test "redirects to show when data is valid", %{conn: conn} do
|
|
||||||
conn = post(conn, ~p"/products", product: @create_attrs)
|
|
||||||
|
|
||||||
assert %{id: id} = redirected_params(conn)
|
|
||||||
assert redirected_to(conn) == ~p"/products/#{id}"
|
|
||||||
|
|
||||||
conn = get(conn, ~p"/products/#{id}")
|
|
||||||
assert html_response(conn, 200) =~ "Product #{id}"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "renders errors when data is invalid", %{conn: conn} do
|
|
||||||
conn = post(conn, ~p"/products", product: @invalid_attrs)
|
|
||||||
assert html_response(conn, 200) =~ "New Product"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "edit product" do
|
|
||||||
setup [:create_product]
|
|
||||||
|
|
||||||
test "renders form for editing chosen product", %{conn: conn, product: product} do
|
|
||||||
conn = get(conn, ~p"/products/#{product}/edit")
|
|
||||||
assert html_response(conn, 200) =~ "Edit Product"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "update product" do
|
|
||||||
setup [:create_product]
|
|
||||||
|
|
||||||
test "redirects when data is valid", %{conn: conn, product: product} do
|
|
||||||
conn = put(conn, ~p"/products/#{product}", product: @update_attrs)
|
|
||||||
assert redirected_to(conn) == ~p"/products/#{product}"
|
|
||||||
|
|
||||||
conn = get(conn, ~p"/products/#{product}")
|
|
||||||
assert html_response(conn, 200) =~ "some updated description"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "renders errors when data is invalid", %{conn: conn, product: product} do
|
|
||||||
conn = put(conn, ~p"/products/#{product}", product: @invalid_attrs)
|
|
||||||
assert html_response(conn, 200) =~ "Edit Product"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "delete product" do
|
|
||||||
setup [:create_product]
|
|
||||||
|
|
||||||
test "deletes chosen product", %{conn: conn, product: product} do
|
|
||||||
conn = delete(conn, ~p"/products/#{product}")
|
|
||||||
assert redirected_to(conn) == ~p"/products"
|
|
||||||
|
|
||||||
assert_error_sent 404, fn ->
|
|
||||||
get(conn, ~p"/products/#{product}")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp create_product(_) do
|
|
||||||
product = product_fixture()
|
|
||||||
%{product: product}
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,113 +0,0 @@
|
||||||
defmodule BrightWeb.UserSessionControllerTest do
|
|
||||||
use BrightWeb.ConnCase, async: true
|
|
||||||
|
|
||||||
import Bright.AccountsFixtures
|
|
||||||
|
|
||||||
setup do
|
|
||||||
%{user: user_fixture()}
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "POST /users/log_in" do
|
|
||||||
test "logs the user in", %{conn: conn, user: user} do
|
|
||||||
conn =
|
|
||||||
post(conn, ~p"/users/log_in", %{
|
|
||||||
"user" => %{"email" => user.email, "password" => valid_user_password()}
|
|
||||||
})
|
|
||||||
|
|
||||||
assert get_session(conn, :user_token)
|
|
||||||
assert redirected_to(conn) == ~p"/"
|
|
||||||
|
|
||||||
# Now do a logged in request and assert on the menu
|
|
||||||
conn = get(conn, ~p"/")
|
|
||||||
response = html_response(conn, 200)
|
|
||||||
assert response =~ user.email
|
|
||||||
assert response =~ ~p"/users/settings"
|
|
||||||
assert response =~ ~p"/users/log_out"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "logs the user in with remember me", %{conn: conn, user: user} do
|
|
||||||
conn =
|
|
||||||
post(conn, ~p"/users/log_in", %{
|
|
||||||
"user" => %{
|
|
||||||
"email" => user.email,
|
|
||||||
"password" => valid_user_password(),
|
|
||||||
"remember_me" => "true"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
assert conn.resp_cookies["_bright_web_user_remember_me"]
|
|
||||||
assert redirected_to(conn) == ~p"/"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "logs the user in with return to", %{conn: conn, user: user} do
|
|
||||||
conn =
|
|
||||||
conn
|
|
||||||
|> init_test_session(user_return_to: "/foo/bar")
|
|
||||||
|> post(~p"/users/log_in", %{
|
|
||||||
"user" => %{
|
|
||||||
"email" => user.email,
|
|
||||||
"password" => valid_user_password()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
assert redirected_to(conn) == "/foo/bar"
|
|
||||||
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Welcome back!"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "login following registration", %{conn: conn, user: user} do
|
|
||||||
conn =
|
|
||||||
conn
|
|
||||||
|> post(~p"/users/log_in", %{
|
|
||||||
"_action" => "registered",
|
|
||||||
"user" => %{
|
|
||||||
"email" => user.email,
|
|
||||||
"password" => valid_user_password()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
assert redirected_to(conn) == ~p"/"
|
|
||||||
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Account created successfully"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "login following password update", %{conn: conn, user: user} do
|
|
||||||
conn =
|
|
||||||
conn
|
|
||||||
|> post(~p"/users/log_in", %{
|
|
||||||
"_action" => "password_updated",
|
|
||||||
"user" => %{
|
|
||||||
"email" => user.email,
|
|
||||||
"password" => valid_user_password()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
assert redirected_to(conn) == ~p"/users/settings"
|
|
||||||
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Password updated successfully"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "redirects to login page with invalid credentials", %{conn: conn} do
|
|
||||||
conn =
|
|
||||||
post(conn, ~p"/users/log_in", %{
|
|
||||||
"user" => %{"email" => "invalid@email.com", "password" => "invalid_password"}
|
|
||||||
})
|
|
||||||
|
|
||||||
assert Phoenix.Flash.get(conn.assigns.flash, :error) == "Invalid email or password"
|
|
||||||
assert redirected_to(conn) == ~p"/users/log_in"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "DELETE /users/log_out" do
|
|
||||||
test "logs the user out", %{conn: conn, user: user} do
|
|
||||||
conn = conn |> log_in_user(user) |> delete(~p"/users/log_out")
|
|
||||||
assert redirected_to(conn) == ~p"/"
|
|
||||||
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 = delete(conn, ~p"/users/log_out")
|
|
||||||
assert redirected_to(conn) == ~p"/"
|
|
||||||
refute get_session(conn, :user_token)
|
|
||||||
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Logged out successfully"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,67 +0,0 @@
|
||||||
defmodule BrightWeb.UserConfirmationInstructionsLiveTest do
|
|
||||||
use BrightWeb.ConnCase, async: true
|
|
||||||
|
|
||||||
import Phoenix.LiveViewTest
|
|
||||||
import Bright.AccountsFixtures
|
|
||||||
|
|
||||||
alias Bright.Accounts
|
|
||||||
alias Bright.Repo
|
|
||||||
|
|
||||||
setup do
|
|
||||||
%{user: user_fixture()}
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "Resend confirmation" do
|
|
||||||
test "renders the resend confirmation page", %{conn: conn} do
|
|
||||||
{:ok, _lv, html} = live(conn, ~p"/users/confirm")
|
|
||||||
assert html =~ "Resend confirmation instructions"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "sends a new confirmation token", %{conn: conn, user: user} do
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/confirm")
|
|
||||||
|
|
||||||
{:ok, conn} =
|
|
||||||
lv
|
|
||||||
|> form("#resend_confirmation_form", user: %{email: user.email})
|
|
||||||
|> render_submit()
|
|
||||||
|> follow_redirect(conn, ~p"/")
|
|
||||||
|
|
||||||
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 user is confirmed", %{conn: conn, user: user} do
|
|
||||||
Repo.update!(Accounts.User.confirm_changeset(user))
|
|
||||||
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/confirm")
|
|
||||||
|
|
||||||
{:ok, conn} =
|
|
||||||
lv
|
|
||||||
|> form("#resend_confirmation_form", user: %{email: user.email})
|
|
||||||
|> render_submit()
|
|
||||||
|> follow_redirect(conn, ~p"/")
|
|
||||||
|
|
||||||
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
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/confirm")
|
|
||||||
|
|
||||||
{:ok, conn} =
|
|
||||||
lv
|
|
||||||
|> form("#resend_confirmation_form", user: %{email: "unknown@example.com"})
|
|
||||||
|> render_submit()
|
|
||||||
|> follow_redirect(conn, ~p"/")
|
|
||||||
|
|
||||||
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
|
|
||||||
"If your email is in our system"
|
|
||||||
|
|
||||||
assert Repo.all(Accounts.UserToken) == []
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,89 +0,0 @@
|
||||||
defmodule BrightWeb.UserConfirmationLiveTest do
|
|
||||||
use BrightWeb.ConnCase, async: true
|
|
||||||
|
|
||||||
import Phoenix.LiveViewTest
|
|
||||||
import Bright.AccountsFixtures
|
|
||||||
|
|
||||||
alias Bright.Accounts
|
|
||||||
alias Bright.Repo
|
|
||||||
|
|
||||||
setup do
|
|
||||||
%{user: user_fixture()}
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "Confirm user" do
|
|
||||||
test "renders confirmation page", %{conn: conn} do
|
|
||||||
{:ok, _lv, html} = live(conn, ~p"/users/confirm/some-token")
|
|
||||||
assert html =~ "Confirm Account"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "confirms the given token once", %{conn: conn, user: user} do
|
|
||||||
token =
|
|
||||||
extract_user_token(fn url ->
|
|
||||||
Accounts.deliver_user_confirmation_instructions(user, url)
|
|
||||||
end)
|
|
||||||
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/confirm/#{token}")
|
|
||||||
|
|
||||||
result =
|
|
||||||
lv
|
|
||||||
|> form("#confirmation_form")
|
|
||||||
|> render_submit()
|
|
||||||
|> follow_redirect(conn, "/")
|
|
||||||
|
|
||||||
assert {:ok, conn} = result
|
|
||||||
|
|
||||||
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
|
|
||||||
"User confirmed successfully"
|
|
||||||
|
|
||||||
assert Accounts.get_user!(user.id).confirmed_at
|
|
||||||
refute get_session(conn, :user_token)
|
|
||||||
assert Repo.all(Accounts.UserToken) == []
|
|
||||||
|
|
||||||
# when not logged in
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/confirm/#{token}")
|
|
||||||
|
|
||||||
result =
|
|
||||||
lv
|
|
||||||
|> form("#confirmation_form")
|
|
||||||
|> render_submit()
|
|
||||||
|> follow_redirect(conn, "/")
|
|
||||||
|
|
||||||
assert {:ok, conn} = result
|
|
||||||
|
|
||||||
assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
|
|
||||||
"User confirmation link is invalid or it has expired"
|
|
||||||
|
|
||||||
# when logged in
|
|
||||||
conn =
|
|
||||||
build_conn()
|
|
||||||
|> log_in_user(user)
|
|
||||||
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/confirm/#{token}")
|
|
||||||
|
|
||||||
result =
|
|
||||||
lv
|
|
||||||
|> form("#confirmation_form")
|
|
||||||
|> render_submit()
|
|
||||||
|> follow_redirect(conn, "/")
|
|
||||||
|
|
||||||
assert {:ok, conn} = result
|
|
||||||
refute Phoenix.Flash.get(conn.assigns.flash, :error)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not confirm email with invalid token", %{conn: conn, user: user} do
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/confirm/invalid-token")
|
|
||||||
|
|
||||||
{:ok, conn} =
|
|
||||||
lv
|
|
||||||
|> form("#confirmation_form")
|
|
||||||
|> render_submit()
|
|
||||||
|> follow_redirect(conn, ~p"/")
|
|
||||||
|
|
||||||
assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
|
|
||||||
"User confirmation link is invalid or it has expired"
|
|
||||||
|
|
||||||
refute Accounts.get_user!(user.id).confirmed_at
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,63 +0,0 @@
|
||||||
defmodule BrightWeb.UserForgotPasswordLiveTest do
|
|
||||||
use BrightWeb.ConnCase, async: true
|
|
||||||
|
|
||||||
import Phoenix.LiveViewTest
|
|
||||||
import Bright.AccountsFixtures
|
|
||||||
|
|
||||||
alias Bright.Accounts
|
|
||||||
alias Bright.Repo
|
|
||||||
|
|
||||||
describe "Forgot password page" do
|
|
||||||
test "renders email page", %{conn: conn} do
|
|
||||||
{:ok, lv, html} = live(conn, ~p"/users/reset_password")
|
|
||||||
|
|
||||||
assert html =~ "Forgot your password?"
|
|
||||||
assert has_element?(lv, ~s|a[href="#{~p"/users/register"}"]|, "Register")
|
|
||||||
assert has_element?(lv, ~s|a[href="#{~p"/users/log_in"}"]|, "Log in")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "redirects if already logged in", %{conn: conn} do
|
|
||||||
result =
|
|
||||||
conn
|
|
||||||
|> log_in_user(user_fixture())
|
|
||||||
|> live(~p"/users/reset_password")
|
|
||||||
|> follow_redirect(conn, ~p"/")
|
|
||||||
|
|
||||||
assert {:ok, _conn} = result
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "Reset link" do
|
|
||||||
setup do
|
|
||||||
%{user: user_fixture()}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "sends a new reset password token", %{conn: conn, user: user} do
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/reset_password")
|
|
||||||
|
|
||||||
{:ok, conn} =
|
|
||||||
lv
|
|
||||||
|> form("#reset_password_form", user: %{"email" => user.email})
|
|
||||||
|> render_submit()
|
|
||||||
|> follow_redirect(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
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/reset_password")
|
|
||||||
|
|
||||||
{:ok, conn} =
|
|
||||||
lv
|
|
||||||
|> form("#reset_password_form", user: %{"email" => "unknown@example.com"})
|
|
||||||
|> render_submit()
|
|
||||||
|> follow_redirect(conn, "/")
|
|
||||||
|
|
||||||
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "If your email is in our system"
|
|
||||||
assert Repo.all(Accounts.UserToken) == []
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,87 +0,0 @@
|
||||||
defmodule BrightWeb.UserLoginLiveTest do
|
|
||||||
use BrightWeb.ConnCase, async: true
|
|
||||||
|
|
||||||
import Phoenix.LiveViewTest
|
|
||||||
import Bright.AccountsFixtures
|
|
||||||
|
|
||||||
describe "Log in page" do
|
|
||||||
test "renders log in page", %{conn: conn} do
|
|
||||||
{:ok, _lv, html} = live(conn, ~p"/users/log_in")
|
|
||||||
|
|
||||||
assert html =~ "Log in"
|
|
||||||
assert html =~ "Register"
|
|
||||||
assert html =~ "Forgot your password?"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "redirects if already logged in", %{conn: conn} do
|
|
||||||
result =
|
|
||||||
conn
|
|
||||||
|> log_in_user(user_fixture())
|
|
||||||
|> live(~p"/users/log_in")
|
|
||||||
|> follow_redirect(conn, "/")
|
|
||||||
|
|
||||||
assert {:ok, _conn} = result
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "user login" do
|
|
||||||
test "redirects if user login with valid credentials", %{conn: conn} do
|
|
||||||
password = "123456789abcd"
|
|
||||||
user = user_fixture(%{password: password})
|
|
||||||
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/log_in")
|
|
||||||
|
|
||||||
form =
|
|
||||||
form(lv, "#login_form", user: %{email: user.email, password: password, remember_me: true})
|
|
||||||
|
|
||||||
conn = submit_form(form, conn)
|
|
||||||
|
|
||||||
assert redirected_to(conn) == ~p"/"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "redirects to login page with a flash error if there are no valid credentials", %{
|
|
||||||
conn: conn
|
|
||||||
} do
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/log_in")
|
|
||||||
|
|
||||||
form =
|
|
||||||
form(lv, "#login_form",
|
|
||||||
user: %{email: "test@email.com", password: "123456", remember_me: true}
|
|
||||||
)
|
|
||||||
|
|
||||||
conn = submit_form(form, conn)
|
|
||||||
|
|
||||||
assert Phoenix.Flash.get(conn.assigns.flash, :error) == "Invalid email or password"
|
|
||||||
|
|
||||||
assert redirected_to(conn) == "/users/log_in"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "login navigation" do
|
|
||||||
test "redirects to registration page when the Register button is clicked", %{conn: conn} do
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/log_in")
|
|
||||||
|
|
||||||
{:ok, _login_live, login_html} =
|
|
||||||
lv
|
|
||||||
|> element(~s|main a:fl-contains("Sign up")|)
|
|
||||||
|> render_click()
|
|
||||||
|> follow_redirect(conn, ~p"/users/register")
|
|
||||||
|
|
||||||
assert login_html =~ "Register"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "redirects to forgot password page when the Forgot Password button is clicked", %{
|
|
||||||
conn: conn
|
|
||||||
} do
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/log_in")
|
|
||||||
|
|
||||||
{:ok, conn} =
|
|
||||||
lv
|
|
||||||
|> element(~s|main a:fl-contains("Forgot your password?")|)
|
|
||||||
|> render_click()
|
|
||||||
|> follow_redirect(conn, ~p"/users/reset_password")
|
|
||||||
|
|
||||||
assert conn.resp_body =~ "Forgot your password?"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,87 +0,0 @@
|
||||||
defmodule BrightWeb.UserRegistrationLiveTest do
|
|
||||||
use BrightWeb.ConnCase, async: true
|
|
||||||
|
|
||||||
import Phoenix.LiveViewTest
|
|
||||||
import Bright.AccountsFixtures
|
|
||||||
|
|
||||||
describe "Registration page" do
|
|
||||||
test "renders registration page", %{conn: conn} do
|
|
||||||
{:ok, _lv, html} = live(conn, ~p"/users/register")
|
|
||||||
|
|
||||||
assert html =~ "Register"
|
|
||||||
assert html =~ "Log in"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "redirects if already logged in", %{conn: conn} do
|
|
||||||
result =
|
|
||||||
conn
|
|
||||||
|> log_in_user(user_fixture())
|
|
||||||
|> live(~p"/users/register")
|
|
||||||
|> follow_redirect(conn, "/")
|
|
||||||
|
|
||||||
assert {:ok, _conn} = result
|
|
||||||
end
|
|
||||||
|
|
||||||
test "renders errors for invalid data", %{conn: conn} do
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/register")
|
|
||||||
|
|
||||||
result =
|
|
||||||
lv
|
|
||||||
|> element("#registration_form")
|
|
||||||
|> render_change(user: %{"email" => "with spaces", "password" => "too short"})
|
|
||||||
|
|
||||||
assert result =~ "Register"
|
|
||||||
assert result =~ "must have the @ sign and no spaces"
|
|
||||||
assert result =~ "should be at least 12 character"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "register user" do
|
|
||||||
test "creates account and logs the user in", %{conn: conn} do
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/register")
|
|
||||||
|
|
||||||
email = unique_user_email()
|
|
||||||
form = form(lv, "#registration_form", user: valid_user_attributes(email: email))
|
|
||||||
render_submit(form)
|
|
||||||
conn = follow_trigger_action(form, conn)
|
|
||||||
|
|
||||||
assert redirected_to(conn) == ~p"/"
|
|
||||||
|
|
||||||
# Now do a logged in request and assert on the menu
|
|
||||||
conn = get(conn, "/")
|
|
||||||
response = html_response(conn, 200)
|
|
||||||
assert response =~ email
|
|
||||||
assert response =~ "Settings"
|
|
||||||
assert response =~ "Log out"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "renders errors for duplicated email", %{conn: conn} do
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/register")
|
|
||||||
|
|
||||||
user = user_fixture(%{email: "test@email.com"})
|
|
||||||
|
|
||||||
result =
|
|
||||||
lv
|
|
||||||
|> form("#registration_form",
|
|
||||||
user: %{"email" => user.email, "password" => "valid_password"}
|
|
||||||
)
|
|
||||||
|> render_submit()
|
|
||||||
|
|
||||||
assert result =~ "has already been taken"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "registration navigation" do
|
|
||||||
test "redirects to login page when the Log in button is clicked", %{conn: conn} do
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/register")
|
|
||||||
|
|
||||||
{:ok, _login_live, login_html} =
|
|
||||||
lv
|
|
||||||
|> element(~s|main a:fl-contains("Log in")|)
|
|
||||||
|> render_click()
|
|
||||||
|> follow_redirect(conn, ~p"/users/log_in")
|
|
||||||
|
|
||||||
assert login_html =~ "Log in"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,118 +0,0 @@
|
||||||
defmodule BrightWeb.UserResetPasswordLiveTest do
|
|
||||||
use BrightWeb.ConnCase, async: true
|
|
||||||
|
|
||||||
import Phoenix.LiveViewTest
|
|
||||||
import Bright.AccountsFixtures
|
|
||||||
|
|
||||||
alias Bright.Accounts
|
|
||||||
|
|
||||||
setup do
|
|
||||||
user = user_fixture()
|
|
||||||
|
|
||||||
token =
|
|
||||||
extract_user_token(fn url ->
|
|
||||||
Accounts.deliver_user_reset_password_instructions(user, url)
|
|
||||||
end)
|
|
||||||
|
|
||||||
%{token: token, user: user}
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "Reset password page" do
|
|
||||||
test "renders reset password with valid token", %{conn: conn, token: token} do
|
|
||||||
{:ok, _lv, html} = live(conn, ~p"/users/reset_password/#{token}")
|
|
||||||
|
|
||||||
assert html =~ "Reset Password"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not render reset password with invalid token", %{conn: conn} do
|
|
||||||
{:error, {:redirect, to}} = live(conn, ~p"/users/reset_password/invalid")
|
|
||||||
|
|
||||||
assert to == %{
|
|
||||||
flash: %{"error" => "Reset password link is invalid or it has expired."},
|
|
||||||
to: ~p"/"
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "renders errors for invalid data", %{conn: conn, token: token} do
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/reset_password/#{token}")
|
|
||||||
|
|
||||||
result =
|
|
||||||
lv
|
|
||||||
|> element("#reset_password_form")
|
|
||||||
|> render_change(
|
|
||||||
user: %{"password" => "secret12", "password_confirmation" => "secret123456"}
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result =~ "should be at least 12 character"
|
|
||||||
assert result =~ "does not match password"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "Reset Password" do
|
|
||||||
test "resets password once", %{conn: conn, token: token, user: user} do
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/reset_password/#{token}")
|
|
||||||
|
|
||||||
{:ok, conn} =
|
|
||||||
lv
|
|
||||||
|> form("#reset_password_form",
|
|
||||||
user: %{
|
|
||||||
"password" => "new valid password",
|
|
||||||
"password_confirmation" => "new valid password"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|> render_submit()
|
|
||||||
|> follow_redirect(conn, ~p"/users/log_in")
|
|
||||||
|
|
||||||
refute get_session(conn, :user_token)
|
|
||||||
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Password reset successfully"
|
|
||||||
assert Accounts.get_user_by_email_and_password(user.email, "new valid password")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not reset password on invalid data", %{conn: conn, token: token} do
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/reset_password/#{token}")
|
|
||||||
|
|
||||||
result =
|
|
||||||
lv
|
|
||||||
|> form("#reset_password_form",
|
|
||||||
user: %{
|
|
||||||
"password" => "too short",
|
|
||||||
"password_confirmation" => "does not match"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|> render_submit()
|
|
||||||
|
|
||||||
assert result =~ "Reset Password"
|
|
||||||
assert result =~ "should be at least 12 character(s)"
|
|
||||||
assert result =~ "does not match password"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "Reset password navigation" do
|
|
||||||
test "redirects to login page when the Log in button is clicked", %{conn: conn, token: token} do
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/reset_password/#{token}")
|
|
||||||
|
|
||||||
{:ok, conn} =
|
|
||||||
lv
|
|
||||||
|> element(~s|main a:fl-contains("Log in")|)
|
|
||||||
|> render_click()
|
|
||||||
|> follow_redirect(conn, ~p"/users/log_in")
|
|
||||||
|
|
||||||
assert conn.resp_body =~ "Log in"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "redirects to registration page when the Register button is clicked", %{
|
|
||||||
conn: conn,
|
|
||||||
token: token
|
|
||||||
} do
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/reset_password/#{token}")
|
|
||||||
|
|
||||||
{:ok, conn} =
|
|
||||||
lv
|
|
||||||
|> element(~s|main a:fl-contains("Register")|)
|
|
||||||
|> render_click()
|
|
||||||
|> follow_redirect(conn, ~p"/users/register")
|
|
||||||
|
|
||||||
assert conn.resp_body =~ "Register"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,210 +0,0 @@
|
||||||
defmodule BrightWeb.UserSettingsLiveTest do
|
|
||||||
use BrightWeb.ConnCase, async: true
|
|
||||||
|
|
||||||
alias Bright.Accounts
|
|
||||||
import Phoenix.LiveViewTest
|
|
||||||
import Bright.AccountsFixtures
|
|
||||||
|
|
||||||
describe "Settings page" do
|
|
||||||
test "renders settings page", %{conn: conn} do
|
|
||||||
{:ok, _lv, html} =
|
|
||||||
conn
|
|
||||||
|> log_in_user(user_fixture())
|
|
||||||
|> live(~p"/users/settings")
|
|
||||||
|
|
||||||
assert html =~ "Change Email"
|
|
||||||
assert html =~ "Change Password"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "redirects if user is not logged in", %{conn: conn} do
|
|
||||||
assert {:error, redirect} = live(conn, ~p"/users/settings")
|
|
||||||
|
|
||||||
assert {:redirect, %{to: path, flash: flash}} = redirect
|
|
||||||
assert path == ~p"/users/log_in"
|
|
||||||
assert %{"error" => "You must log in to access this page."} = flash
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "update email form" do
|
|
||||||
setup %{conn: conn} do
|
|
||||||
password = valid_user_password()
|
|
||||||
user = user_fixture(%{password: password})
|
|
||||||
%{conn: log_in_user(conn, user), user: user, password: password}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "updates the user email", %{conn: conn, password: password, user: user} do
|
|
||||||
new_email = unique_user_email()
|
|
||||||
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
|
||||||
|
|
||||||
result =
|
|
||||||
lv
|
|
||||||
|> form("#email_form", %{
|
|
||||||
"current_password" => password,
|
|
||||||
"user" => %{"email" => new_email}
|
|
||||||
})
|
|
||||||
|> render_submit()
|
|
||||||
|
|
||||||
assert result =~ "A link to confirm your email"
|
|
||||||
assert Accounts.get_user_by_email(user.email)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "renders errors with invalid data (phx-change)", %{conn: conn} do
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
|
||||||
|
|
||||||
result =
|
|
||||||
lv
|
|
||||||
|> element("#email_form")
|
|
||||||
|> render_change(%{
|
|
||||||
"action" => "update_email",
|
|
||||||
"current_password" => "invalid",
|
|
||||||
"user" => %{"email" => "with spaces"}
|
|
||||||
})
|
|
||||||
|
|
||||||
assert result =~ "Change Email"
|
|
||||||
assert result =~ "must have the @ sign and no spaces"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "renders errors with invalid data (phx-submit)", %{conn: conn, user: user} do
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
|
||||||
|
|
||||||
result =
|
|
||||||
lv
|
|
||||||
|> form("#email_form", %{
|
|
||||||
"current_password" => "invalid",
|
|
||||||
"user" => %{"email" => user.email}
|
|
||||||
})
|
|
||||||
|> render_submit()
|
|
||||||
|
|
||||||
assert result =~ "Change Email"
|
|
||||||
assert result =~ "did not change"
|
|
||||||
assert result =~ "is not valid"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "update password form" do
|
|
||||||
setup %{conn: conn} do
|
|
||||||
password = valid_user_password()
|
|
||||||
user = user_fixture(%{password: password})
|
|
||||||
%{conn: log_in_user(conn, user), user: user, password: password}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "updates the user password", %{conn: conn, user: user, password: password} do
|
|
||||||
new_password = valid_user_password()
|
|
||||||
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
|
||||||
|
|
||||||
form =
|
|
||||||
form(lv, "#password_form", %{
|
|
||||||
"current_password" => password,
|
|
||||||
"user" => %{
|
|
||||||
"email" => user.email,
|
|
||||||
"password" => new_password,
|
|
||||||
"password_confirmation" => new_password
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
render_submit(form)
|
|
||||||
|
|
||||||
new_password_conn = follow_trigger_action(form, conn)
|
|
||||||
|
|
||||||
assert redirected_to(new_password_conn) == ~p"/users/settings"
|
|
||||||
|
|
||||||
assert get_session(new_password_conn, :user_token) != get_session(conn, :user_token)
|
|
||||||
|
|
||||||
assert Phoenix.Flash.get(new_password_conn.assigns.flash, :info) =~
|
|
||||||
"Password updated successfully"
|
|
||||||
|
|
||||||
assert Accounts.get_user_by_email_and_password(user.email, new_password)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "renders errors with invalid data (phx-change)", %{conn: conn} do
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
|
||||||
|
|
||||||
result =
|
|
||||||
lv
|
|
||||||
|> element("#password_form")
|
|
||||||
|> render_change(%{
|
|
||||||
"current_password" => "invalid",
|
|
||||||
"user" => %{
|
|
||||||
"password" => "too short",
|
|
||||||
"password_confirmation" => "does not match"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
assert result =~ "Change Password"
|
|
||||||
assert result =~ "should be at least 12 character(s)"
|
|
||||||
assert result =~ "does not match password"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "renders errors with invalid data (phx-submit)", %{conn: conn} do
|
|
||||||
{:ok, lv, _html} = live(conn, ~p"/users/settings")
|
|
||||||
|
|
||||||
result =
|
|
||||||
lv
|
|
||||||
|> form("#password_form", %{
|
|
||||||
"current_password" => "invalid",
|
|
||||||
"user" => %{
|
|
||||||
"password" => "too short",
|
|
||||||
"password_confirmation" => "does not match"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|> render_submit()
|
|
||||||
|
|
||||||
assert result =~ "Change Password"
|
|
||||||
assert result =~ "should be at least 12 character(s)"
|
|
||||||
assert result =~ "does not match password"
|
|
||||||
assert result =~ "is not valid"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "confirm email" do
|
|
||||||
setup %{conn: conn} do
|
|
||||||
user = user_fixture()
|
|
||||||
email = unique_user_email()
|
|
||||||
|
|
||||||
token =
|
|
||||||
extract_user_token(fn url ->
|
|
||||||
Accounts.deliver_user_update_email_instructions(%{user | email: email}, user.email, url)
|
|
||||||
end)
|
|
||||||
|
|
||||||
%{conn: log_in_user(conn, user), token: token, email: email, user: user}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "updates the user email once", %{conn: conn, user: user, token: token, email: email} do
|
|
||||||
{:error, redirect} = live(conn, ~p"/users/settings/confirm_email/#{token}")
|
|
||||||
|
|
||||||
assert {:live_redirect, %{to: path, flash: flash}} = redirect
|
|
||||||
assert path == ~p"/users/settings"
|
|
||||||
assert %{"info" => message} = flash
|
|
||||||
assert message == "Email changed successfully."
|
|
||||||
refute Accounts.get_user_by_email(user.email)
|
|
||||||
assert Accounts.get_user_by_email(email)
|
|
||||||
|
|
||||||
# use confirm token again
|
|
||||||
{:error, redirect} = live(conn, ~p"/users/settings/confirm_email/#{token}")
|
|
||||||
assert {:live_redirect, %{to: path, flash: flash}} = redirect
|
|
||||||
assert path == ~p"/users/settings"
|
|
||||||
assert %{"error" => message} = flash
|
|
||||||
assert message == "Email change link is invalid or it has expired."
|
|
||||||
end
|
|
||||||
|
|
||||||
test "does not update email with invalid token", %{conn: conn, user: user} do
|
|
||||||
{:error, redirect} = live(conn, ~p"/users/settings/confirm_email/oops")
|
|
||||||
assert {:live_redirect, %{to: path, flash: flash}} = redirect
|
|
||||||
assert path == ~p"/users/settings"
|
|
||||||
assert %{"error" => message} = flash
|
|
||||||
assert message == "Email change link is invalid or it has expired."
|
|
||||||
assert Accounts.get_user_by_email(user.email)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "redirects if user is not logged in", %{token: token} do
|
|
||||||
conn = build_conn()
|
|
||||||
{:error, redirect} = live(conn, ~p"/users/settings/confirm_email/#{token}")
|
|
||||||
assert {:redirect, %{to: path, flash: flash}} = redirect
|
|
||||||
assert path == ~p"/users/log_in"
|
|
||||||
assert %{"error" => message} = flash
|
|
||||||
assert message == "You must log in to access this page."
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 1.9 MiB |
Binary file not shown.
After Width: | Height: | Size: 1.9 MiB |
|
@ -4,10 +4,12 @@ defmodule Bright.PlatformsFixtures do
|
||||||
entities via the `Bright.Platforms` context.
|
entities via the `Bright.Platforms` context.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def platform_fixture(attrs \\ %{})
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Generate a platform.
|
Generate a platform.
|
||||||
"""
|
"""
|
||||||
def platform_fixture(attrs \\ %{}) do
|
def platform_fixture(attrs) do
|
||||||
{:ok, platform} =
|
{:ok, platform} =
|
||||||
attrs
|
attrs
|
||||||
|> Enum.into(%{
|
|> Enum.into(%{
|
||||||
|
@ -20,6 +22,15 @@ defmodule Bright.PlatformsFixtures do
|
||||||
platform
|
platform
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def platform_fixture(attrs) do
|
||||||
|
{:ok, platform} =
|
||||||
|
attrs
|
||||||
|
|> valid_platform_attributes()
|
||||||
|
|> Bright.Platforms.register_platform()
|
||||||
|
|
||||||
|
platform
|
||||||
|
end
|
||||||
|
|
||||||
def unique_platform_email, do: "platform#{System.unique_integer()}@example.com"
|
def unique_platform_email, do: "platform#{System.unique_integer()}@example.com"
|
||||||
def valid_platform_password, do: "hello world!"
|
def valid_platform_password, do: "hello world!"
|
||||||
|
|
||||||
|
@ -30,14 +41,7 @@ defmodule Bright.PlatformsFixtures do
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
def platform_fixture(attrs \\ %{}) do
|
|
||||||
{:ok, platform} =
|
|
||||||
attrs
|
|
||||||
|> valid_platform_attributes()
|
|
||||||
|> Bright.Platforms.register_platform()
|
|
||||||
|
|
||||||
platform
|
|
||||||
end
|
|
||||||
|
|
||||||
def extract_platform_token(fun) do
|
def extract_platform_token(fun) do
|
||||||
{:ok, captured_email} = fun.(&"[TOKEN]#{&1}[TOKEN]")
|
{:ok, captured_email} = fun.(&"[TOKEN]#{&1}[TOKEN]")
|
||||||
|
|
Loading…
Reference in New Issue