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/test test
|
||||
|
||||
|
||||
# compile assets
|
||||
RUN mix assets.deploy
|
||||
|
@ -75,7 +77,7 @@ RUN mix release
|
|||
|
||||
## dev target
|
||||
FROM builder AS dev
|
||||
RUN echo "balls. that is all."
|
||||
COPY ./services/bright/config/test.exs config/test.exs
|
||||
RUN ls -la ./contrib/
|
||||
CMD [ "mix", "phx.server" ]
|
||||
|
||||
|
|
|
@ -11,6 +11,8 @@ config :bright,
|
|||
ecto_repos: [Bright.Repo],
|
||||
generators: [timestamp_type: :utc_datetime]
|
||||
|
||||
|
||||
|
||||
# Configures the endpoint
|
||||
config :bright, BrightWeb.Endpoint,
|
||||
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
|
||||
#
|
||||
# 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
|
||||
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
|
||||
database_url =
|
||||
System.get_env("DATABASE_URL") ||
|
||||
|
@ -51,6 +73,7 @@ if config_env() == :prod do
|
|||
host = System.get_env("PHX_HOST") || "example.com"
|
||||
port = String.to_integer(System.get_env("PORT") || "4000")
|
||||
|
||||
|
||||
config :bright, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
|
||||
|
||||
config :bright, BrightWeb.Endpoint,
|
||||
|
@ -65,18 +88,23 @@ if config_env() == :prod do
|
|||
],
|
||||
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,
|
||||
# client_id: System.get_env("PATREON_CLIENT_ID"),
|
||||
# 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,
|
||||
# client_id: {:system, "GITHUB_CLIENT_ID"},
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import Config
|
||||
|
||||
|
||||
|
||||
|
||||
# Only in tests, remove the complexity from the password hashing algorithm
|
||||
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.
|
||||
# Run `mix help test` for more information.
|
||||
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")}",
|
||||
pool: Ecto.Adapters.SQL.Sandbox,
|
||||
pool_size: System.schedulers_online() * 2
|
||||
|
@ -22,7 +27,8 @@ config :bright, BrightWeb.Endpoint,
|
|||
server: false
|
||||
|
||||
# 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
|
||||
config :bright, superstreamer_api_client: ApiClientBehaviorMock
|
||||
|
@ -42,3 +48,10 @@ config :phoenix, :plug_init_mode, :runtime
|
|||
# Enable helpful, but potentially expensive runtime checks
|
||||
config :phoenix_live_view,
|
||||
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
|
||||
|
||||
alias Bright.Repo
|
||||
|
@ -9,12 +9,16 @@ defmodule Bright.Jobs.CreateHlsPlaylist do
|
|||
|
||||
require Logger
|
||||
|
||||
@auth_token System.get_env("SUPERSTREAMER_AUTH_TOKEN")
|
||||
@api_url System.get_env("SUPERSTREAMER_URL")
|
||||
@public_s3_endpoint System.get_env("PUBLIC_S3_ENDPOINT")
|
||||
@auth_token Application.get_env(:bright, :superstreamer_auth_token)
|
||||
@superstreamer_url Application.get_env(:bright, :superstreamer_url)
|
||||
@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
|
||||
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)
|
||||
|
||||
payload = build_payload(input_url)
|
||||
|
@ -39,12 +43,12 @@ defmodule Bright.Jobs.CreateHlsPlaylist do
|
|||
%{
|
||||
"inputs" => [
|
||||
%{"type" => "audio", "path" => input_url, "language" => "eng"},
|
||||
%{"type" => "video", "path" => input_url}
|
||||
# %{"type" => "video", "path" => input_url}
|
||||
],
|
||||
"streams" => [
|
||||
%{"type" => "video", "codec" => "h264", "height" => 1080},
|
||||
%{"type" => "video", "codec" => "h264", "height" => 720},
|
||||
%{"type" => "video", "codec" => "h264", "height" => 144},
|
||||
# %{"type" => "video", "codec" => "h264", "height" => 1080},
|
||||
# %{"type" => "video", "codec" => "h264", "height" => 720},
|
||||
# %{"type" => "video", "codec" => "h264", "height" => 144},
|
||||
%{"type" => "audio", "codec" => "aac"}
|
||||
],
|
||||
"tag" => "create_hls_playlist"
|
||||
|
@ -54,14 +58,19 @@ defmodule Bright.Jobs.CreateHlsPlaylist do
|
|||
|
||||
defp start_transcode(payload) do
|
||||
Logger.info("Starting transcode with payload: #{inspect(payload)}")
|
||||
IO.puts "Starting transcode with payload: #{inspect(payload)}"
|
||||
|
||||
headers = auth_headers()
|
||||
|
||||
Logger.info("auth headers as follows")
|
||||
Logger.info(inspect(headers))
|
||||
|
||||
Logger.info("now we will POST /transcode to api_url=#{@api_url}")
|
||||
data = case HTTPoison.post("#{@api_url}/transcode", Jason.encode!(payload), headers) do
|
||||
if is_nil(@superstreamer_url) 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, Jason.decode!(body)}
|
||||
|
||||
|
@ -71,6 +80,9 @@ defmodule Bright.Jobs.CreateHlsPlaylist do
|
|||
{:error, %HTTPoison.Error{reason: reason}} ->
|
||||
{:error, reason}
|
||||
|
||||
[] ->
|
||||
{:error, "We got an empty response from Superstreamer"}
|
||||
|
||||
failed ->
|
||||
Logger.error("Failed to POST /transcode: #{inspect(failed)}")
|
||||
{:error, :failed}
|
||||
|
@ -104,8 +116,8 @@ defmodule Bright.Jobs.CreateHlsPlaylist do
|
|||
Logger.info("auth headers as follows")
|
||||
Logger.info(inspect(headers))
|
||||
|
||||
Logger.info("now we will POST /package to api_url=#{@api_url}")
|
||||
data = case HTTPoison.post("#{@api_url}/package", Jason.encode!(payload), headers) do
|
||||
Logger.info("now we will POST /package to superstreamer_url=#{@superstreamer_url}")
|
||||
data = case HTTPoison.post("#{@superstreamer_url}/package", Jason.encode!(payload), headers) do
|
||||
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
|
||||
{:ok, Jason.decode!(body)}
|
||||
|
||||
|
@ -161,14 +173,14 @@ defmodule Bright.Jobs.CreateHlsPlaylist do
|
|||
Logger.info(">>>> formatted=#{inspect(formatted)}")
|
||||
{:halt, {:ok, formatted["assetId"]}}
|
||||
|
||||
{:ok, "failed", _data} ->
|
||||
{:halt, {:error, "superstreamer reports that the job failed."}}
|
||||
|
||||
{:ok, state, data} ->
|
||||
Logger.info("Job ID #{job_id} #{state}. Re-polling in #{poll_interval}.")
|
||||
:timer.sleep(poll_interval)
|
||||
{:cont, acc}
|
||||
|
||||
{:ok, "failed", _data} ->
|
||||
{:halt, {:error, "superstreamer reports that the job failed."}}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Error polling job ID #{job_id}: #{inspect(reason)}")
|
||||
{:halt, {:error, reason}}
|
||||
|
@ -179,7 +191,7 @@ defmodule Bright.Jobs.CreateHlsPlaylist do
|
|||
defp get_job_status(job_id) do
|
||||
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, Jason.decode!(body)}
|
||||
|
||||
|
@ -208,7 +220,7 @@ defmodule Bright.Jobs.CreateHlsPlaylist do
|
|||
|
||||
defp update_vod_with_playlist_url(vod, asset_id) do
|
||||
playlist_url = generate_playlist_url(asset_id)
|
||||
|
||||
Logger.info("playlist_url=#{playlist_url}")
|
||||
vod
|
||||
|> Ecto.Changeset.change(playlist_url: playlist_url)
|
||||
|> 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.
|
||||
"""
|
||||
|
||||
require Logger
|
||||
import Ecto.Query, warn: false
|
||||
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
|
||||
|
||||
|
||||
|
||||
if origin_temp_input_url do
|
||||
|
||||
|
||||
%{id: id, origin_temp_input_url: origin_temp_input_url}
|
||||
|> Bright.Jobs.ProcessVod.new()
|
||||
|> Bright.ObanWorkers.ProcessVod.new()
|
||||
|> Oban.insert()
|
||||
|
||||
end
|
||||
|
|
|
@ -14,6 +14,7 @@ defmodule Bright.Streams.Vod do
|
|||
field :ipfs_cid, :string
|
||||
field :torrent, :string
|
||||
field :notes, :string
|
||||
field :thumbnail_url, :string
|
||||
|
||||
belongs_to :stream, Bright.Streams.Stream
|
||||
|
||||
|
@ -23,7 +24,7 @@ defmodule Bright.Streams.Vod do
|
|||
@doc false
|
||||
def changeset(vod, attrs) do
|
||||
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])
|
||||
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
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Bright.Repo
|
||||
alias Bright.{Repo, Regexp}
|
||||
|
||||
schema "users" do
|
||||
field :name, :string
|
||||
field :email, :string
|
||||
field :is_admin, :boolean
|
||||
field :auth_token, :string
|
||||
field :auth_token_expires_at, :utc_datetime
|
||||
|
@ -17,11 +16,79 @@ defmodule Bright.User do
|
|||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
|
||||
|
||||
@doc false
|
||||
def changeset(user, attrs) do
|
||||
user
|
||||
|> cast(attrs, [:name, :email, :bio, :is_admin])
|
||||
|> validate_required([:name, :email, :bio])
|
||||
|> cast(attrs, [:name, :patreon_handle, :github_handle, :is_admin])
|
||||
|> 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
|
||||
|
||||
|
||||
|
@ -29,8 +96,8 @@ defmodule Bright.User do
|
|||
Repo.get_by(__MODULE__, github_handle: handle)
|
||||
end
|
||||
|
||||
def get_by_ueberauth(%{provider: :patreon, info: %{nickname: handle}}) do
|
||||
Repo.get_by(__MODULE__, patreon_handle: handle)
|
||||
def get_by_ueberauth(%{provider: :patreon, info: %{id: patreon_id}}) do
|
||||
Repo.get_by(__MODULE__, patreon_handle: patreon_id)
|
||||
end
|
||||
|
||||
def get_by_ueberauth(_), do: nil
|
||||
|
@ -45,4 +112,34 @@ defmodule Bright.User do
|
|||
end
|
||||
|
||||
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
|
||||
|
|
|
@ -29,7 +29,7 @@ defmodule Bright.UserFromAuth do
|
|||
|
||||
# default case if nothing matches
|
||||
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))
|
||||
nil
|
||||
end
|
||||
|
|
|
@ -102,4 +102,9 @@ defmodule Bright.Vtubers do
|
|||
Vtuber.changeset(vtuber, attrs)
|
||||
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
|
||||
|
|
|
@ -15,9 +15,9 @@ defmodule BrightWeb.CoreComponents do
|
|||
Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage.
|
||||
"""
|
||||
use Phoenix.Component
|
||||
use Gettext, backend: BrightWeb.Gettext
|
||||
|
||||
alias Phoenix.LiveView.JS
|
||||
import BrightWeb.Gettext
|
||||
|
||||
@doc """
|
||||
Renders a modal.
|
||||
|
@ -233,10 +233,7 @@ def button(assigns) do
|
|||
type={@type}
|
||||
class={[
|
||||
"button",
|
||||
"is-primary",
|
||||
"is-rounded",
|
||||
"py-2", "px-3",
|
||||
@class
|
||||
"is-primary"
|
||||
]}
|
||||
{@rest}
|
||||
>
|
||||
|
@ -275,6 +272,7 @@ end
|
|||
attr :id, :any, default: nil
|
||||
attr :name, :any
|
||||
attr :label, :string, default: nil
|
||||
attr :help, :string, default: nil
|
||||
attr :value, :any
|
||||
|
||||
attr :type, :string,
|
||||
|
@ -314,7 +312,7 @@ end
|
|||
|
||||
~H"""
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">
|
||||
<label class="checkbox">
|
||||
<input type="hidden" name={@name} value="false" disabled={@rest[:disabled]} />
|
||||
<input
|
||||
type="checkbox"
|
||||
|
@ -322,7 +320,6 @@ end
|
|||
name={@name}
|
||||
value="true"
|
||||
checked={@checked}
|
||||
class="input"
|
||||
{@rest}
|
||||
/>
|
||||
<%= @label %>
|
||||
|
@ -393,6 +390,7 @@ end
|
|||
]}
|
||||
{@rest}
|
||||
/>
|
||||
<.help for={@id}><%= @help %></.help>
|
||||
<.error :for={msg <- @errors}><%= msg %></.error>
|
||||
</div>
|
||||
"""
|
||||
|
@ -412,6 +410,19 @@ 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 """
|
||||
Generates a generic error message.
|
||||
"""
|
||||
|
|
|
@ -11,10 +11,176 @@ defmodule BrightWeb.AuthController do
|
|||
plug Ueberauth
|
||||
|
||||
alias Ueberauth.Strategy.Helpers
|
||||
alias Bright.UserFromAuth
|
||||
alias Bright.User
|
||||
alias Bright.{Repo, 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
|
||||
conn
|
||||
|> put_flash(:info, "You have been logged out!")
|
||||
|
@ -28,30 +194,59 @@ defmodule BrightWeb.AuthController do
|
|||
|> redirect(to: "/")
|
||||
end
|
||||
|
||||
# def callback(conn, %{"provider" => provider}) do
|
||||
# end
|
||||
|
||||
|
||||
def callback(conn = %{assigns: %{ueberauth_auth: auth}}, _params) do
|
||||
if user = User.get_by_ueberauth(auth) do
|
||||
sign_in_and_redirect(conn, user, ~p"/~")
|
||||
else
|
||||
conn
|
||||
|> put_flash(:success, "Almost there! Please complete your profile now.")
|
||||
|> redirect(to: ~p"/join?#{params_from_ueberauth(auth)}")
|
||||
|
||||
user_params = %{
|
||||
github_handle: Map.get(auth, "nickname", nil),
|
||||
patreon_handle: Map.get(auth, "full_name", nil),
|
||||
name: "test"
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
|
||||
|
||||
defp sign_in_and_redirect(conn, user, route) do
|
||||
Logger.info("sign_in_and_redirect with user=#{inspect(user)}")
|
||||
|
||||
user
|
||||
|> User.sign_in_changes()
|
||||
|> Repo.update()
|
||||
|
||||
conn
|
||||
|> assign(:current_user, user)
|
||||
|> put_flash(:success, "Welcome!")
|
||||
|> put_flash(:success, "Welcome to Futureporn!")
|
||||
|> put_session("id", user.id)
|
||||
|> configure_session(renew: true)
|
||||
|> redirect(to: route)
|
||||
|
@ -59,34 +254,14 @@ defmodule BrightWeb.AuthController 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
|
||||
|
||||
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
|
||||
|
||||
# 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
|
||||
# {: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
|
||||
defp signed_in_path(_conn), do: ~p"/"
|
||||
|
||||
end
|
||||
|
|
|
@ -1,15 +1,59 @@
|
|||
defmodule BrightWeb.UserController do
|
||||
use BrightWeb, :controller
|
||||
alias Bright.{User, Repo}
|
||||
require Logger
|
||||
|
||||
def index(conn, _params) do
|
||||
render(conn, :index)
|
||||
end
|
||||
|
||||
# def show(conn) do
|
||||
# conn
|
||||
# |> render(:show)
|
||||
# end
|
||||
|
||||
def show(conn) do
|
||||
conn
|
||||
|> render(:show)
|
||||
def show(conn = %{assigns: %{current_user: me}}, _params) do
|
||||
Logger.info(">>> me=#{inspect(me)}")
|
||||
render(conn, :show, changeset: User.update_changeset(me))
|
||||
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
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
use Gettext, otp_app: :bright
|
||||
use Gettext.Backend, otp_app: :bright
|
||||
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
|
||||
use BrightWeb, :router
|
||||
|
||||
import BrightWeb.UserAuth
|
||||
|
||||
import BrightWeb.AuthController
|
||||
|
||||
pipeline :browser do
|
||||
plug :accepts, ["html", "json"]
|
||||
plug :fetch_session
|
||||
plug :fetch_live_flash
|
||||
plug :put_root_layout, html: {BrightWeb.Layouts, :root}
|
||||
plug :protect_from_forgery
|
||||
plug :put_secure_browser_headers
|
||||
plug :fetch_current_user
|
||||
plug(:accepts, ["html", "json"])
|
||||
plug(:fetch_session)
|
||||
plug(:fetch_live_flash)
|
||||
plug(:put_root_layout, html: {BrightWeb.Layouts, :root})
|
||||
plug(:protect_from_forgery)
|
||||
plug(:put_secure_browser_headers)
|
||||
plug(:fetch_current_user)
|
||||
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
|
||||
plug :accepts, ["json"]
|
||||
plug(:accepts, ["json"])
|
||||
end
|
||||
|
||||
|
||||
|
||||
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)
|
||||
get "/platforms/new", PlatformController, :new
|
||||
post "/platforms", PlatformController, :create
|
||||
get "/platforms/:id/edit", PlatformController, :edit
|
||||
patch "/platforms/:id", PlatformController, :update
|
||||
put "/platforms/:id", PlatformController, :update
|
||||
|
||||
get("/platforms/new", PlatformController, :new)
|
||||
post("/platforms", PlatformController, :create)
|
||||
get("/platforms/:id/edit", PlatformController, :edit)
|
||||
patch("/platforms/:id", PlatformController, :update)
|
||||
put("/platforms/:id", PlatformController, :update)
|
||||
end
|
||||
|
||||
scope "/auth", BrightWeb do
|
||||
pipe_through :browser
|
||||
pipe_through(:browser)
|
||||
|
||||
get "/:provider", AuthController, :request
|
||||
get "/:provider/callback", AuthController, :callback
|
||||
post "/:provider/callback", AuthController, :callback
|
||||
delete "/logout", AuthController, :delete
|
||||
get("/:provider", AuthController, :request)
|
||||
get("/:provider/callback", AuthController, :callback)
|
||||
post("/:provider/callback", AuthController, :callback)
|
||||
delete("/logout", AuthController, :delete)
|
||||
end
|
||||
|
||||
|
||||
scope "/" do
|
||||
pipe_through [:browser, :require_authenticated_user]
|
||||
pipe_through([:browser, :require_authenticated_user])
|
||||
|
||||
get "/streams/new", StreamController, :new
|
||||
post "/streams", StreamController, :create
|
||||
get("/streams/new", StreamController, :new)
|
||||
post("/streams", StreamController, :create)
|
||||
|
||||
# get "/vods/new", VodController, :new
|
||||
# post "/vods", VodController, :create
|
||||
|
@ -69,65 +68,54 @@ defmodule BrightWeb.Router do
|
|||
# get "/vtubers/:id/edit", VtuberController, :edit
|
||||
# end
|
||||
|
||||
get "/profile", UserController, :show
|
||||
|
||||
|
||||
get "/tags/new", TagController, :new
|
||||
post "/tags", TagController, :create
|
||||
|
||||
get("/tags/new", TagController, :new)
|
||||
post("/tags", TagController, :create)
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
scope "/", BrightWeb do
|
||||
pipe_through :browser
|
||||
pipe_through(:browser)
|
||||
|
||||
get "/", PageController, :home
|
||||
get("/", PageController, :home)
|
||||
|
||||
get "/patrons", PatronController, :index
|
||||
get "/about", PageController, :about
|
||||
get "/api", PageController, :api
|
||||
get("/profile", UserController, :show, as: :user)
|
||||
|
||||
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
|
||||
get "/streams/:id", StreamController, :show
|
||||
resources("/orders", OrderController, only: [:create, :show])
|
||||
|
||||
get("/streams", StreamController, :index)
|
||||
get("/streams/:id", StreamController, :show)
|
||||
|
||||
resources "/vods", VodController
|
||||
get "/vods/:id", VodController, :show
|
||||
get "/vods", VodController, :index
|
||||
resources("/vods", VodController)
|
||||
get("/vods/:id", VodController, :show)
|
||||
get("/vods", VodController, :index)
|
||||
|
||||
get("/tags", TagController, :index)
|
||||
get("/tags:id", TagController, :show)
|
||||
|
||||
get "/tags", TagController, :index
|
||||
get "/tags:id", TagController, :show
|
||||
get("/platforms", PlatformController, :index)
|
||||
get("/platforms/:id", PlatformController, :show)
|
||||
|
||||
get "/platforms", PlatformController, :index
|
||||
get "/platforms/:id", PlatformController, :show
|
||||
|
||||
|
||||
|
||||
get "/vtubers", VtuberController, :index
|
||||
get "/vtubers/:id", VtuberController, :show
|
||||
get("/vtubers", VtuberController, :index)
|
||||
get("/vtubers/:id", VtuberController, :show)
|
||||
|
||||
resources "/vt", VtuberController do
|
||||
get "/vods", VodController, :index
|
||||
get "/vods/:id", VodController, :show
|
||||
get("/vods", VodController, :index)
|
||||
get("/vods/:id", VodController, :show)
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
end
|
||||
|
||||
# Other scopes may use custom stacks.
|
||||
scope "/api", BrightWeb do
|
||||
pipe_through :api
|
||||
resources "/urls", UrlController, except: [:new, :edit]
|
||||
get "/health", PageController, :health
|
||||
pipe_through(:api)
|
||||
resources("/urls", UrlController, except: [:new, :edit])
|
||||
get("/health", PageController, :health)
|
||||
end
|
||||
|
||||
# Enable LiveDashboard and Swoosh mailbox preview in development
|
||||
|
@ -140,23 +128,19 @@ defmodule BrightWeb.Router do
|
|||
import Phoenix.LiveDashboard.Router
|
||||
|
||||
scope "/dev" do
|
||||
pipe_through :browser
|
||||
pipe_through(:browser)
|
||||
|
||||
live_dashboard "/dashboard", metrics: BrightWeb.Telemetry
|
||||
forward "/mailbox", Plug.Swoosh.MailboxPreview
|
||||
live_dashboard("/dashboard", metrics: BrightWeb.Telemetry)
|
||||
forward("/mailbox", Plug.Swoosh.MailboxPreview)
|
||||
end
|
||||
end
|
||||
|
||||
## Authentication routes
|
||||
|
||||
scope "/", BrightWeb do
|
||||
pipe_through [:browser]
|
||||
|
||||
pipe_through([:browser])
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
## Authentication routes
|
||||
|
||||
# scope "/", BrightWeb do
|
||||
|
@ -173,25 +157,25 @@ defmodule BrightWeb.Router do
|
|||
# post "/users/log_in", UserSessionController, :create
|
||||
# end
|
||||
|
||||
scope "/", BrightWeb do
|
||||
pipe_through [:browser, :require_authenticated_user]
|
||||
# scope "/", BrightWeb do
|
||||
# pipe_through [:browser, :require_authenticated_user]
|
||||
|
||||
live_session :require_authenticated_user,
|
||||
on_mount: [{BrightWeb.UserAuth, :ensure_authenticated}] do
|
||||
live "/users/settings", UserSettingsLive, :edit
|
||||
live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
|
||||
end
|
||||
end
|
||||
# live_session :require_authenticated_user,
|
||||
# on_mount: [{BrightWeb.UserAuth, :ensure_authenticated}] do
|
||||
# live "/users/settings", UserSettingsLive, :edit
|
||||
# live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
|
||||
# end
|
||||
# end
|
||||
|
||||
scope "/", BrightWeb do
|
||||
pipe_through [:browser]
|
||||
# scope "/", BrightWeb do
|
||||
# pipe_through [:browser]
|
||||
|
||||
delete "/users/log_out", UserSessionController, :delete
|
||||
# delete "/users/log_out", UserSessionController, :delete
|
||||
|
||||
live_session :current_user,
|
||||
on_mount: [{BrightWeb.UserAuth, :mount_current_user}] do
|
||||
live "/users/confirm/:token", UserConfirmationLive, :edit
|
||||
live "/users/confirm", UserConfirmationInstructionsLive, :new
|
||||
end
|
||||
end
|
||||
# live_session :current_user,
|
||||
# on_mount: [{BrightWeb.UserAuth, :mount_current_user}] do
|
||||
# live "/users/confirm/:token", UserConfirmationLive, :edit
|
||||
# live "/users/confirm", UserConfirmationInstructionsLive, :new
|
||||
# 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_github, "~> 0.8"},
|
||||
{:timex, "~> 3.0"},
|
||||
{:ex_aws_s3, "~> 2.0"},
|
||||
{:ex_aws, "~> 2.1"},
|
||||
{:ffmpex, "~> 0.11.0"},
|
||||
{:sweet_xml, "~> 0.6"}
|
||||
]
|
||||
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"},
|
||||
"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"},
|
||||
"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"},
|
||||
"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"},
|
||||
"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"},
|
||||
|
@ -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_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"},
|
||||
"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"},
|
||||
"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"]},
|
||||
"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"},
|
||||
"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"},
|
||||
|
|
|
@ -3,7 +3,7 @@ defmodule Bright.Repo.Migrations.AddPlatformToUser do
|
|||
|
||||
def change do
|
||||
alter table(:users) do
|
||||
add :is_admin, :boolean, default: false, null: false
|
||||
add :platform, :string, null: false
|
||||
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"
|
||||
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
|
||||
vtuber = vtuber_fixture()
|
||||
|
@ -105,57 +102,12 @@ defmodule Bright.VtubersTest do
|
|||
vtuber = vtuber_fixture()
|
||||
assert %Ecto.Changeset{} = Vtubers.change_vtuber(vtuber)
|
||||
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
|
||||
assert {:error, %Ecto.Changeset{}} = Vtubers.create_vtuber(@invalid_attrs)
|
||||
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
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
||||
def platform_fixture(attrs \\ %{})
|
||||
|
||||
@doc """
|
||||
Generate a platform.
|
||||
"""
|
||||
def platform_fixture(attrs \\ %{}) do
|
||||
def platform_fixture(attrs) do
|
||||
{:ok, platform} =
|
||||
attrs
|
||||
|> Enum.into(%{
|
||||
|
@ -20,6 +22,15 @@ defmodule Bright.PlatformsFixtures do
|
|||
platform
|
||||
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 valid_platform_password, do: "hello world!"
|
||||
|
||||
|
@ -30,14 +41,7 @@ defmodule Bright.PlatformsFixtures do
|
|||
})
|
||||
end
|
||||
|
||||
def platform_fixture(attrs \\ %{}) do
|
||||
{:ok, platform} =
|
||||
attrs
|
||||
|> valid_platform_attributes()
|
||||
|> Bright.Platforms.register_platform()
|
||||
|
||||
platform
|
||||
end
|
||||
|
||||
def extract_platform_token(fun) do
|
||||
{:ok, captured_email} = fun.(&"[TOKEN]#{&1}[TOKEN]")
|
||||
|
|
Loading…
Reference in New Issue