diff --git a/dockerfiles/bright.dockerfile b/dockerfiles/bright.dockerfile index eb222a1..51413ee 100644 --- a/dockerfiles/bright.dockerfile +++ b/dockerfiles/bright.dockerfile @@ -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" ] diff --git a/services/bright/config/config.exs b/services/bright/config/config.exs index 00101b4..1a4d2db 100644 --- a/services/bright/config/config.exs +++ b/services/bright/config/config.exs @@ -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 diff --git a/services/bright/config/runtime.exs b/services/bright/config/runtime.exs index b75a279..08531d5 100644 --- a/services/bright/config/runtime.exs +++ b/services/bright/config/runtime.exs @@ -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"}, diff --git a/services/bright/config/test.exs b/services/bright/config/test.exs index fe9305d..932dc3f 100644 --- a/services/bright/config/test.exs +++ b/services/bright/config/test.exs @@ -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) diff --git a/services/bright/lib/bright/accounts.ex b/services/bright/lib/bright/accounts.ex deleted file mode 100644 index 3715aa5..0000000 --- a/services/bright/lib/bright/accounts.ex +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright/accounts/user.ex b/services/bright/lib/bright/accounts/user.ex deleted file mode 100644 index da1799f..0000000 --- a/services/bright/lib/bright/accounts/user.ex +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright/accounts/user_notifier.ex b/services/bright/lib/bright/accounts/user_notifier.ex deleted file mode 100644 index c047d86..0000000 --- a/services/bright/lib/bright/accounts/user_notifier.ex +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright/accounts/user_token.ex b/services/bright/lib/bright/accounts/user_token.ex deleted file mode 100644 index 0bf73d6..0000000 --- a/services/bright/lib/bright/accounts/user_token.ex +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright/b2.ex b/services/bright/lib/bright/b2.ex new file mode 100644 index 0000000..d7e6f27 --- /dev/null +++ b/services/bright/lib/bright/b2.ex @@ -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 diff --git a/services/bright/lib/bright/cache.ex b/services/bright/lib/bright/cache.ex new file mode 100644 index 0000000..d0af26b --- /dev/null +++ b/services/bright/lib/bright/cache.ex @@ -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 diff --git a/services/bright/lib/bright/images.ex b/services/bright/lib/bright/images.ex new file mode 100644 index 0000000..b6af1aa --- /dev/null +++ b/services/bright/lib/bright/images.ex @@ -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 diff --git a/services/bright/lib/bright/jobs/create_hls_playlist.ex.fuck b/services/bright/lib/bright/jobs/create_hls_playlist.ex.fuck deleted file mode 100644 index f731b00..0000000 --- a/services/bright/lib/bright/jobs/create_hls_playlist.ex.fuck +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright/jobs/process_vod.ex b/services/bright/lib/bright/jobs/process_vod.ex deleted file mode 100644 index c6ae3fe..0000000 --- a/services/bright/lib/bright/jobs/process_vod.ex +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright/jobs/create_hls_playlist.ex b/services/bright/lib/bright/oban_workers/create_hls_playlist.ex similarity index 78% rename from services/bright/lib/bright/jobs/create_hls_playlist.ex rename to services/bright/lib/bright/oban_workers/create_hls_playlist.ex index 0fd5e8c..b1d5c3f 100644 --- a/services/bright/lib/bright/jobs/create_hls_playlist.ex +++ b/services/bright/lib/bright/oban_workers/create_hls_playlist.ex @@ -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!() diff --git a/services/bright/lib/bright/oban_workers/create_s3_asset.ex b/services/bright/lib/bright/oban_workers/create_s3_asset.ex new file mode 100644 index 0000000..f828eda --- /dev/null +++ b/services/bright/lib/bright/oban_workers/create_s3_asset.ex @@ -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: <> + # 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 diff --git a/services/bright/lib/bright/oban_workers/create_thumbnail.ex b/services/bright/lib/bright/oban_workers/create_thumbnail.ex new file mode 100644 index 0000000..d6e449c --- /dev/null +++ b/services/bright/lib/bright/oban_workers/create_thumbnail.ex @@ -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 diff --git a/services/bright/lib/bright/oban_workers/process_vod.ex b/services/bright/lib/bright/oban_workers/process_vod.ex new file mode 100644 index 0000000..645477b --- /dev/null +++ b/services/bright/lib/bright/oban_workers/process_vod.ex @@ -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 diff --git a/services/bright/lib/bright/regexp.ex b/services/bright/lib/bright/regexp.ex new file mode 100644 index 0000000..faef2ac --- /dev/null +++ b/services/bright/lib/bright/regexp.ex @@ -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/(?<\/?)(?.*?)(?>)/ + + def tag(name), do: ~r/(?<\/?)#{name}(?>)/ + + 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 diff --git a/services/bright/lib/bright/streams.ex b/services/bright/lib/bright/streams.ex index 20c44a7..b4d008c 100644 --- a/services/bright/lib/bright/streams.ex +++ b/services/bright/lib/bright/streams.ex @@ -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 diff --git a/services/bright/lib/bright/streams/vod.ex b/services/bright/lib/bright/streams/vod.ex index d23e3c5..a99c471 100644 --- a/services/bright/lib/bright/streams/vod.ex +++ b/services/bright/lib/bright/streams/vod.ex @@ -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 diff --git a/services/bright/lib/bright/uploads.ex b/services/bright/lib/bright/uploads.ex new file mode 100644 index 0000000..2a027b3 --- /dev/null +++ b/services/bright/lib/bright/uploads.ex @@ -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 diff --git a/services/bright/lib/bright/user.ex b/services/bright/lib/bright/user.ex index 797c53f..3e04b20 100644 --- a/services/bright/lib/bright/user.ex +++ b/services/bright/lib/bright/user.ex @@ -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 diff --git a/services/bright/lib/bright/user_from_auth.ex b/services/bright/lib/bright/user_from_auth.ex index b18e951..440f1b6 100644 --- a/services/bright/lib/bright/user_from_auth.ex +++ b/services/bright/lib/bright/user_from_auth.ex @@ -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 diff --git a/services/bright/lib/bright/vtubers.ex b/services/bright/lib/bright/vtubers.ex index 2202b8f..0cc7130 100644 --- a/services/bright/lib/bright/vtubers.ex +++ b/services/bright/lib/bright/vtubers.ex @@ -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 diff --git a/services/bright/lib/bright_web/components/core_components.ex b/services/bright/lib/bright_web/components/core_components.ex index 61f4679..da7c469 100644 --- a/services/bright/lib/bright_web/components/core_components.ex +++ b/services/bright/lib/bright_web/components/core_components.ex @@ -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"""
-
""" @@ -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""" +

+ <%= render_slot(@inner_block) %> +

+ """ + end + @doc """ Generates a generic error message. """ diff --git a/services/bright/lib/bright_web/controllers/auth_controller.ex b/services/bright/lib/bright_web/controllers/auth_controller.ex index ac88691..5f4c07d 100644 --- a/services/bright/lib/bright_web/controllers/auth_controller.ex +++ b/services/bright/lib/bright_web/controllers/auth_controller.ex @@ -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 diff --git a/services/bright/lib/bright_web/controllers/user_controller.ex b/services/bright/lib/bright_web/controllers/user_controller.ex index 55901d7..c9c922b 100644 --- a/services/bright/lib/bright_web/controllers/user_controller.ex +++ b/services/bright/lib/bright_web/controllers/user_controller.ex @@ -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 diff --git a/services/bright/lib/bright_web/controllers/user_html/join.html.heex b/services/bright/lib/bright_web/controllers/user_html/join.html.heex new file mode 100644 index 0000000..ed6e1ab --- /dev/null +++ b/services/bright/lib/bright_web/controllers/user_html/join.html.heex @@ -0,0 +1,7 @@ + +<.header> + Join Futureporn + + + +<.user_form changeset={@changeset} action={~p"/join"} /> \ No newline at end of file diff --git a/services/bright/lib/bright_web/controllers/user_html/user.html.heex b/services/bright/lib/bright_web/controllers/user_html/show.html.heex similarity index 100% rename from services/bright/lib/bright_web/controllers/user_html/user.html.heex rename to services/bright/lib/bright_web/controllers/user_html/show.html.heex diff --git a/services/bright/lib/bright_web/controllers/user_html/user_form.html.heex b/services/bright/lib/bright_web/controllers/user_html/user_form.html.heex new file mode 100644 index 0000000..989e9bd --- /dev/null +++ b/services/bright/lib/bright_web/controllers/user_html/user_form.html.heex @@ -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. + + <.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 + + + diff --git a/services/bright/lib/bright_web/controllers/user_session_controller.ex b/services/bright/lib/bright_web/controllers/user_session_controller.ex deleted file mode 100644 index bb69007..0000000 --- a/services/bright/lib/bright_web/controllers/user_session_controller.ex +++ /dev/null @@ -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 diff --git a/services/bright/lib/bright_web/gettext.ex b/services/bright/lib/bright_web/gettext.ex index c098c06..3615c9b 100644 --- a/services/bright/lib/bright_web/gettext.ex +++ b/services/bright/lib/bright_web/gettext.ex @@ -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 diff --git a/services/bright/lib/bright_web/live/user_confirmation_instructions_live.ex b/services/bright/lib/bright_web/live/user_confirmation_instructions_live.ex deleted file mode 100644 index 9e0c9b7..0000000 --- a/services/bright/lib/bright_web/live/user_confirmation_instructions_live.ex +++ /dev/null @@ -1,51 +0,0 @@ -defmodule BrightWeb.UserConfirmationInstructionsLive do - use BrightWeb, :live_view - - alias Bright.Accounts - - def render(assigns) do - ~H""" -
- <.header class="text-center"> - No confirmation instructions received? - <:subtitle>We'll send a new confirmation link to your inbox - - - <.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 - - - - -

- <.link href={~p"/users/register"}>Register - | <.link href={~p"/users/log_in"}>Log in -

-
- """ - 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 diff --git a/services/bright/lib/bright_web/live/user_confirmation_live.ex b/services/bright/lib/bright_web/live/user_confirmation_live.ex deleted file mode 100644 index e62885e..0000000 --- a/services/bright/lib/bright_web/live/user_confirmation_live.ex +++ /dev/null @@ -1,58 +0,0 @@ -defmodule BrightWeb.UserConfirmationLive do - use BrightWeb, :live_view - - alias Bright.Accounts - - def render(%{live_action: :edit} = assigns) do - ~H""" -
- <.header class="text-center">Confirm Account - - <.simple_form for={@form} id="confirmation_form" phx-submit="confirm_account"> - - <:actions> - <.button phx-disable-with="Confirming..." class="w-full">Confirm my account - - - -

- <.link href={~p"/users/register"}>Register - | <.link href={~p"/users/log_in"}>Log in -

-
- """ - 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 diff --git a/services/bright/lib/bright_web/live/user_forgot_password_live.ex b/services/bright/lib/bright_web/live/user_forgot_password_live.ex deleted file mode 100644 index 0a202b3..0000000 --- a/services/bright/lib/bright_web/live/user_forgot_password_live.ex +++ /dev/null @@ -1,50 +0,0 @@ -defmodule BrightWeb.UserForgotPasswordLive do - use BrightWeb, :live_view - - alias Bright.Accounts - - def render(assigns) do - ~H""" -
- <.header class="text-center"> - Forgot your password? - <:subtitle>We'll send a password reset link to your inbox - - - <.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 - - - -

- <.link href={~p"/users/register"}>Register - | <.link href={~p"/users/log_in"}>Log in -

-
- """ - 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 diff --git a/services/bright/lib/bright_web/live/user_login_live.ex b/services/bright/lib/bright_web/live/user_login_live.ex deleted file mode 100644 index ff10836..0000000 --- a/services/bright/lib/bright_web/live/user_login_live.ex +++ /dev/null @@ -1,43 +0,0 @@ -defmodule BrightWeb.UserLoginLive do - use BrightWeb, :live_view - - def render(assigns) do - ~H""" -
- <.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 - - for an account now. - - - - <.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? - - - <:actions> - <.button phx-disable-with="Logging in..." class="w-full"> - Log in - - - -
- """ - 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 diff --git a/services/bright/lib/bright_web/live/user_registration_live.ex b/services/bright/lib/bright_web/live/user_registration_live.ex deleted file mode 100644 index f4addbb..0000000 --- a/services/bright/lib/bright_web/live/user_registration_live.ex +++ /dev/null @@ -1,87 +0,0 @@ -defmodule BrightWeb.UserRegistrationLive do - use BrightWeb, :live_view - - alias Bright.Accounts - alias Bright.Accounts.User - - def render(assigns) do - ~H""" -
- <.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 - - to your account now. - - - - <.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. - - - <.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 - - -
- """ - 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 diff --git a/services/bright/lib/bright_web/live/user_reset_password_live.ex b/services/bright/lib/bright_web/live/user_reset_password_live.ex deleted file mode 100644 index 0882b88..0000000 --- a/services/bright/lib/bright_web/live/user_reset_password_live.ex +++ /dev/null @@ -1,89 +0,0 @@ -defmodule BrightWeb.UserResetPasswordLive do - use BrightWeb, :live_view - - alias Bright.Accounts - - def render(assigns) do - ~H""" -
- <.header class="text-center">Reset Password - - <.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. - - - <.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 - - - -

- <.link href={~p"/users/register"}>Register - | <.link href={~p"/users/log_in"}>Log in -

-
- """ - 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 diff --git a/services/bright/lib/bright_web/live/user_settings_live.ex b/services/bright/lib/bright_web/live/user_settings_live.ex deleted file mode 100644 index 44e71f2..0000000 --- a/services/bright/lib/bright_web/live/user_settings_live.ex +++ /dev/null @@ -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 - - -
-
- <.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 - - -
-
- <.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 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 - - -
-
- """ - 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 diff --git a/services/bright/lib/bright_web/router.ex b/services/bright/lib/bright_web/router.ex index c246292..f6a9c4a 100644 --- a/services/bright/lib/bright_web/router.ex +++ b/services/bright/lib/bright_web/router.ex @@ -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 diff --git a/services/bright/lib/bright_web/user_auth.ex b/services/bright/lib/bright_web/user_auth.ex deleted file mode 100644 index c7b4e25..0000000 --- a/services/bright/lib/bright_web/user_auth.ex +++ /dev/null @@ -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 diff --git a/services/bright/mix.exs b/services/bright/mix.exs index fc519e8..bee29b2 100644 --- a/services/bright/mix.exs +++ b/services/bright/mix.exs @@ -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 diff --git a/services/bright/mix.lock b/services/bright/mix.lock index 33ddf8e..cee8fe5 100644 --- a/services/bright/mix.lock +++ b/services/bright/mix.lock @@ -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"}, diff --git a/services/bright/priv/repo/migrations/20250115053147_add_provider_to_user.exs b/services/bright/priv/repo/migrations/20250115053147_add_provider_to_user.exs index 8a3522a..1e81407 100644 --- a/services/bright/priv/repo/migrations/20250115053147_add_provider_to_user.exs +++ b/services/bright/priv/repo/migrations/20250115053147_add_provider_to_user.exs @@ -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 diff --git a/services/bright/priv/repo/migrations/20250117132027_remove_email_from_user.exs b/services/bright/priv/repo/migrations/20250117132027_remove_email_from_user.exs new file mode 100644 index 0000000..15bb3ad --- /dev/null +++ b/services/bright/priv/repo/migrations/20250117132027_remove_email_from_user.exs @@ -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 diff --git a/services/bright/priv/repo/migrations/20250117132125_remove_password_from_user.exs b/services/bright/priv/repo/migrations/20250117132125_remove_password_from_user.exs new file mode 100644 index 0000000..acd36bb --- /dev/null +++ b/services/bright/priv/repo/migrations/20250117132125_remove_password_from_user.exs @@ -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 diff --git a/services/bright/priv/repo/migrations/20250117132320_remove_platform_from_user.exs b/services/bright/priv/repo/migrations/20250117132320_remove_platform_from_user.exs new file mode 100644 index 0000000..15788d7 --- /dev/null +++ b/services/bright/priv/repo/migrations/20250117132320_remove_platform_from_user.exs @@ -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 diff --git a/services/bright/priv/repo/migrations/20250120050410_add_thumbnail_url.exs b/services/bright/priv/repo/migrations/20250120050410_add_thumbnail_url.exs new file mode 100644 index 0000000..9bb3cd9 --- /dev/null +++ b/services/bright/priv/repo/migrations/20250120050410_add_thumbnail_url.exs @@ -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 diff --git a/services/bright/test/bright/accounts_test.exs b/services/bright/test/bright/accounts_test.exs deleted file mode 100644 index aa1d894..0000000 --- a/services/bright/test/bright/accounts_test.exs +++ /dev/null @@ -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 diff --git a/services/bright/test/bright/b2_test.exs b/services/bright/test/bright/b2_test.exs new file mode 100644 index 0000000..3c33d84 --- /dev/null +++ b/services/bright/test/bright/b2_test.exs @@ -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 diff --git a/services/bright/test/bright/cache_test.ex b/services/bright/test/bright/cache_test.ex new file mode 100644 index 0000000..8fdf5db --- /dev/null +++ b/services/bright/test/bright/cache_test.ex @@ -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 diff --git a/services/bright/test/bright/images_test.exs b/services/bright/test/bright/images_test.exs new file mode 100644 index 0000000..a4b02b1 --- /dev/null +++ b/services/bright/test/bright/images_test.exs @@ -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: <> + 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: <> + # 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 diff --git a/services/bright/test/bright/oban_workers/create_hls_playlist_test.exs b/services/bright/test/bright/oban_workers/create_hls_playlist_test.exs new file mode 100644 index 0000000..4c19651 --- /dev/null +++ b/services/bright/test/bright/oban_workers/create_hls_playlist_test.exs @@ -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 diff --git a/services/bright/test/bright/oban_workers/create_s3_asset_test.exs b/services/bright/test/bright/oban_workers/create_s3_asset_test.exs new file mode 100644 index 0000000..9db8e08 --- /dev/null +++ b/services/bright/test/bright/oban_workers/create_s3_asset_test.exs @@ -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 diff --git a/services/bright/test/bright/oban_workers/create_thumbnail_test.exs b/services/bright/test/bright/oban_workers/create_thumbnail_test.exs new file mode 100644 index 0000000..8901797 --- /dev/null +++ b/services/bright/test/bright/oban_workers/create_thumbnail_test.exs @@ -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 diff --git a/services/bright/test/bright/uploads_test.exs b/services/bright/test/bright/uploads_test.exs new file mode 100644 index 0000000..f202a25 --- /dev/null +++ b/services/bright/test/bright/uploads_test.exs @@ -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: <> + 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 diff --git a/services/bright/test/bright/vtubers_test.exs b/services/bright/test/bright/vtubers_test.exs index 4ad39d4..abc3f5d 100644 --- a/services/bright/test/bright/vtubers_test.exs +++ b/services/bright/test/bright/vtubers_test.exs @@ -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 diff --git a/services/bright/test/bright_web/controllers/product_controller_test.exs b/services/bright/test/bright_web/controllers/product_controller_test.exs deleted file mode 100644 index 01a1f22..0000000 --- a/services/bright/test/bright_web/controllers/product_controller_test.exs +++ /dev/null @@ -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 diff --git a/services/bright/test/bright_web/controllers/user_session_controller_test.exs b/services/bright/test/bright_web/controllers/user_session_controller_test.exs deleted file mode 100644 index bc96432..0000000 --- a/services/bright/test/bright_web/controllers/user_session_controller_test.exs +++ /dev/null @@ -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 diff --git a/services/bright/test/bright_web/live/user_confirmation_instructions_live_test.exs b/services/bright/test/bright_web/live/user_confirmation_instructions_live_test.exs deleted file mode 100644 index 1ed0b96..0000000 --- a/services/bright/test/bright_web/live/user_confirmation_instructions_live_test.exs +++ /dev/null @@ -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 diff --git a/services/bright/test/bright_web/live/user_confirmation_live_test.exs b/services/bright/test/bright_web/live/user_confirmation_live_test.exs deleted file mode 100644 index 692d7e5..0000000 --- a/services/bright/test/bright_web/live/user_confirmation_live_test.exs +++ /dev/null @@ -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 diff --git a/services/bright/test/bright_web/live/user_forgot_password_live_test.exs b/services/bright/test/bright_web/live/user_forgot_password_live_test.exs deleted file mode 100644 index d4d3e8a..0000000 --- a/services/bright/test/bright_web/live/user_forgot_password_live_test.exs +++ /dev/null @@ -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 diff --git a/services/bright/test/bright_web/live/user_login_live_test.exs b/services/bright/test/bright_web/live/user_login_live_test.exs deleted file mode 100644 index f4bb46f..0000000 --- a/services/bright/test/bright_web/live/user_login_live_test.exs +++ /dev/null @@ -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 diff --git a/services/bright/test/bright_web/live/user_registration_live_test.exs b/services/bright/test/bright_web/live/user_registration_live_test.exs deleted file mode 100644 index e1cc5d5..0000000 --- a/services/bright/test/bright_web/live/user_registration_live_test.exs +++ /dev/null @@ -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 diff --git a/services/bright/test/bright_web/live/user_reset_password_live_test.exs b/services/bright/test/bright_web/live/user_reset_password_live_test.exs deleted file mode 100644 index 46330ec..0000000 --- a/services/bright/test/bright_web/live/user_reset_password_live_test.exs +++ /dev/null @@ -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 diff --git a/services/bright/test/bright_web/live/user_settings_live_test.exs b/services/bright/test/bright_web/live/user_settings_live_test.exs deleted file mode 100644 index cdc04c6..0000000 --- a/services/bright/test/bright_web/live/user_settings_live_test.exs +++ /dev/null @@ -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 diff --git a/services/bright/test/fixtures/SampleVideo_1280x720_1mb.mp4 b/services/bright/test/fixtures/SampleVideo_1280x720_1mb.mp4 new file mode 100644 index 0000000..ed139d6 Binary files /dev/null and b/services/bright/test/fixtures/SampleVideo_1280x720_1mb.mp4 differ diff --git a/services/bright/test/fixtures/thumb.jpg b/services/bright/test/fixtures/thumb.jpg new file mode 100644 index 0000000..2bf3892 Binary files /dev/null and b/services/bright/test/fixtures/thumb.jpg differ diff --git a/services/bright/test/fixtures/thumb2.jpg b/services/bright/test/fixtures/thumb2.jpg new file mode 100644 index 0000000..2bf3892 Binary files /dev/null and b/services/bright/test/fixtures/thumb2.jpg differ diff --git a/services/bright/test/support/fixtures/platforms_fixtures.ex b/services/bright/test/support/fixtures/platforms_fixtures.ex index 301cf7a..eaaa206 100644 --- a/services/bright/test/support/fixtures/platforms_fixtures.ex +++ b/services/bright/test/support/fixtures/platforms_fixtures.ex @@ -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]")