Merge branch 'main' of github.com:jjl/vox_publica into main

This commit is contained in:
Mayel 2020-09-17 11:32:04 +02:00
commit 6613ef3a6e
100 changed files with 2848 additions and 714 deletions

9
.gitignore vendored
View file

@ -32,10 +32,11 @@ npm-debug.log
# The directory NPM downloads your dependencies sources to.
/assets/node_modules/
# Since we are building assets from assets/,
# we ignore priv/static. You may want to comment
# this depending on your deployment strategy.
# Since we are building assets from assets/
/priv/static/
# App and user data
/data/
/data/
# user-local overrides for mess
deps.path

2
.tool-versions Normal file
View file

@ -0,0 +1,2 @@
erlang 23.0.4
elixir 1.10.4-otp-23

13
Makefile Normal file
View file

@ -0,0 +1,13 @@
.PHONY: deps clean-deps update-deps
update-deps:
mix deps.update pointers \
cpub_accounts cpub_blocks cpub_characters cpub_emails \
cpub_local_auth cpub_profiles cpub_users
clean-deps:
mix deps.clean pointers \
cpub_accounts cpub_blocks cpub_characters cpub_emails \
cpub_local_auth cpub_profiles cpub_users --build
deps: update-deps clean-deps

View file

@ -2,6 +2,12 @@
Blogging/Microblogging software.
## Handy commands
* `make update-deps` - updates commonspub dep versions
* `make clean-deps` - cleans the compiled deps so config is reread
* `make deps` - both of the above
## Copyright and License
VoxPublica content publishing platform

View file

@ -1,14 +1,14 @@
// DEFAULT BUTTON MIXIN
@mixin button($bg: var(--color-primary), $color: var(--color-text)) {
@mixin button($bg: var(--color-primary), $color: var(--color-background-0)) {
@include reset;
font-family: inherit;
border-radius: 5px;
background: $bg;
border: 1px solid var(--color-primary-dark);
border: none;
color: $color;
padding: var(--m2) var(--m3);
font-size: 14px;
font-weight: 600;
font-weight: 500;
min-width: 100px;
white-space: nowrap;
vertical-align: middle;
@ -32,19 +32,14 @@
}
@mixin button-classic(
$bg: var(--color-secondary),
$color: var(--color-text)
$bg: var(--color-primary),
$color: var(--color-background-0)
) {
@include button($bg: $bg, $color:$color);
background-color: $bg;
border: 1px solid var(--color-secondary-dark);
border: none;
transition: background-color 0.2s cubic-bezier(0.3, 0, 0.5, 1);
&:hover {
background-color: var(--color-secondary-dark);
text-decoration: none;
}
&:focus {
outline: none;
box-shadow: 0 0 0 4px var(--color-border);

View file

@ -1,13 +1,13 @@
input {
background-color: var(--color-surface);
background-color: var(--color-foreground-2);
border: var(--border);
border-radius: 4px;
height: 34px;
text-indent: 8px;
color: var(--color-text);
color: var(--color-background-0);
&::placeholder {
color: var(--color-text-subtle);
color: var(--color-background-1);
}
}

View file

@ -2,18 +2,38 @@
--fontPrimary: "Fira Sans";
--fontSecondary: "Merriweather";
// DARK THEME
--color-foreground-1: #ECEFF4;
--color-foreground-2: #E5E9F0;
--color-foreground-3: #D8DEE9;
--color-background-0: #2E3440;
--color-background-1: #3B4252;
--color-background-2: #434C5E;
--color-background-3: #4C566A;
// LIGTH THEME
// --color-foreground-1: #2E3440;
// --color-foreground-2: #3B4252;
// --color-foreground-3: #434C5E;
// --color-background-0: #ECEFF4;
// --color-background-1: #E5E9F0;
// --color-background-2: #D8DEE9;
// --color-background-3: #fff;
// color
--color-background: #2e3440;
--color-background: var(--color-background-0);
--color-background-dark: #2a2f3a;
--color-surface: #3b4252;
--color-selection: #434c5e;
--color-border: #4c566a;
--color-text: #eceff4;
--color-text-subtle: #acadaf;
--color-primary: #88c0d0;
--color-primary-dark: #6d9fad;
--color-secondary: #81a1c1;
--color-secondary-dark: #6a849e;
--color-primary: #A0D7E8;
--color-primary-dark: #92c0ce;
--color-secondary: #0D4487;
--color-secondary-dark: #093366;
--color-tertiary: #5e81ac;
--color-tertiary-dark: #4d6c92;
--color-error: #bf616a;
@ -21,6 +41,10 @@
--color-success: #a3be8c;
--color-weird: #b48ead;
// spaces
--m1: 4px;
--m2: 8px;

View file

@ -7,7 +7,8 @@
display: grid;
height: calc(100vh - 60px);
overflow: hidden;
grid-template-columns: 260px 1fr;
// grid-template-columns: 260px 1fr;
grid-template-columns: 1fr;
transition: transform 0.2s ease;
background-color: var(--color-background);
&.full {
@ -17,11 +18,12 @@
.guest__container {
overflow-y: scroll;
height: calc(100vh - 60px);
}
.page.guest {
display: grid;
height: 100vh;
height: calc(100vh - 60px);
grid-template-columns: 1fr;
overflow: overlay;
.page__full {
@ -402,13 +404,18 @@
}
.cpub__header {
height: 60px;
background-color: var(--color-background-dark);
border-bottom: 1px solid var(--color-surface);
position: relative;
z-index: 2;
display: flex;
justify-content: space-between;
nav {
display: flex;
justify-content: space-between;
height: 60px;
background-color: var(--color-background);
border-bottom: 1px solid var(--color-surface);
position: relative;
z-index: 2;
padding: var(--m2)
}
.aside {
aside {
h2 {

View file

@ -1,5 +1,26 @@
use Mix.Config
#### Email configuration
# You will almost certainly want to change at least some of these
alias VoxPublica.Mailer
config :vox_publica, Mailer,
from_address: "noreply@voxpub.local"
alias VoxPublica.Accounts
config :vox_publica, Accounts.Emails,
confirm_email: [subject: "Confirm your email - VoxPublica"],
reset_password: [subject: "Reset your password - VoxPublica"]
#### Pointers configuration
# This tells `Pointers.Tables` which apps to search for tables to
# index. If you add another dependency with Pointables, you will want
# to add it to the search path
config :pointers,
search_path: [
:cpub_activities,
@ -21,6 +42,17 @@ config :pointers,
:vox_publica,
]
#### Flexto Stitching
## WARNING: This is the flaky magic bit. We use configuration to
## compile extra stuff into modules. If you add new fields or
## relations to ecto models in a dependency, you must recompile that
## dependency for it to show up! You will probably find you need to
## `rm -Rf _build/*/lib/cpub_*` a lot.
## Note: This does not apply to configuration for
## `Pointers.Changesets`, which is read at runtime, not compile time
alias CommonsPub.Accounts.{Account, Accounted}
alias CommonsPub.{
Actors.Actor,
@ -71,10 +103,64 @@ config :cpub_users, User,
has_one: [profile: {Profile, foreign_key: :id}],
has_one: [actor: {Actor, foreign_key: :id}]
#### Forms configuration
# You probably will want to leave these
alias VoxPublica.Accounts.{
ChangePasswordForm,
ConfirmEmailForm,
LoginForm,
ResetPasswordForm,
SignupForm,
}
# these are not used yet, but they will be
config :vox_publica, ChangePasswordForm,
cast: [:old_password, :password, :password_confirmation],
required: [:old_password, :password, :password_confirmation],
confirm: :password,
new_password: [length: [min: 10, max: 64]]
config :vox_publica, ConfirmEmailForm,
cast: [:email],
required: [:email],
email: [format: ~r(^[^@]{1,128}@[^@\.]+\.[^@]{2,128}$)]
config :vox_publica, LoginForm,
cast: [:email, :password],
required: [:email, :password],
email: [format: ~r(^[^@]{1,128}@[^@\.]+\.[^@]{2,128}$)],
password: [length: [min: 10, max: 64]]
config :vox_publica, ResetPasswordForm,
cast: [:password, :password_confirmation],
required: [:password, :password_confirmation],
confirm: :password,
password: [length: [min: 10, max: 64]]
config :vox_publica, SignupForm,
cast: [:email, :password],
required: [:email, :password],
email: [format: ~r(^[^@]{1,128}@[^@\.]+\.[^@]{2,128}$)],
password: [length: [min: 10, max: 64]]
alias VoxPublica.Users.CreateForm
config :vox_publica, CreateForm,
username: [format: ~r(^[a-z][a-z0-9_]{2,30}$)i],
name: [length: [min: 3, max: 50]],
summary: [length: [min: 20, max: 500]]
#### Basic configuration
# You probably won't want to touch these. You might override some in
# other config files.
config :vox_publica,
ecto_repos: [VoxPublica.Repo]
# Configures the endpoint
config :vox_publica, VoxPublica.Web.Endpoint,
url: [host: "localhost"],
secret_key_base: "g7K250qlSxhNDt5qnV6f4HFnyoD7fGUuZ8tbBF69aJCOvUIF8P0U7wnnzTqklK10",

View file

@ -1,5 +1,10 @@
use Mix.Config
alias VoxPublica.{Mailer, Repo, Web.Endpoint}
config :vox_publica, Mailer,
adapter: Bamboo.LocalAdapter
# Configure your database
config :vox_publica, VoxPublica.Repo,
username: "postgres",

View file

@ -1,5 +1,10 @@
use Mix.Config
alias VoxPublica.{Mailer, Repo, Web.Endpoint}
config :vox_publica, Mailer,
adapter: Bamboo.TestAdapter
config :logger, level: :warn
# Configure your database
@ -7,7 +12,7 @@ config :logger, level: :warn
# The MIX_TEST_PARTITION environment variable can be used
# to provide built-in test partitioning in CI environment.
# Run `mix help test` for more information.
config :vox_publica, VoxPublica.Repo,
config :vox_publica, Repo,
username: "postgres",
password: "postgres",
database: "vox_publica_test#{System.get_env("MIX_TEST_PARTITION")}",
@ -16,7 +21,7 @@ config :vox_publica, VoxPublica.Repo,
# We don't run a server during test. If one is required,
# you can enable the server option below.
config :vox_publica, VoxPublica.Web.Endpoint,
config :vox_publica, Endpoint,
http: [port: 4002],
server: false

14
deps.git Normal file
View file

@ -0,0 +1,14 @@
activity_pub = "https://gitlab.com/CommonsPub/activitypub.git#develop"
cpub_actors = "https://github.com/commonspub/cpub_actors#main"
pointers = "https://github.com/commonspub/pointers#main"
cpub_accounts = "https://github.com/commonspub/cpub_accounts#main"
# cpub_blocks = "https://github.com/commonspub/cpub_blocks#main"
# cpub_bookmarks = "https://github.com/commonspub/cpub_bookmarks#main"
# cpub_characters = "https://github.com/commonspub/cpub_characters#main"
# cpub_circles = "https://github.com/commonspub/cpub_circles#main"
# cpub_comments = "https://github.com/commonspub/cpub_comments#main"
# cpub_communities = "https://github.com/commonspub/cpub_communities#main"
cpub_emails = "https://github.com/commonspub/cpub_emails#main"
# cpub_local_auth = "https://github.com/commonspub/cpub_local_auth#main"
# cpub_profiles = "https://github.com/commonspub/cpub_profiles#main"
# cpub_users = "https://github.com/commonspub/cpub_users#main"

36
deps.hex Normal file
View file

@ -0,0 +1,36 @@
# web
phoenix_live_view = "~> 0.14"
phoenix_html = "~> 2.11"
phoenix_live_dashboard = "~> 0.2.0"
plug_cowboy = "~> 2.0"
phoenix = "~> 1.5.3"
phoenix_ecto = "~> 4.1"
gettext = "~> 0.11"
jason = "~> 1.0"
# db
ecto_sql = "~> 3.4"
flexto = "~> 0.2.1"
postgrex = ">= 0.0.0"
oban = "~> 2.0.0" # job queueing
pointers_ulid = "~> 0.2"
pointers = "~> 0.5.1"
# email
bamboo = "~> 1.5"
bamboo_smtp = "~> 3.0.0"
# misc
recase = "~> 0.5" # string recasing
faker = "~> 0.14" # fake data generation
telemetry_metrics = "~> 0.4"
telemetry_poller = "~> 0.4"
# plugins
# cpub_accounts = "~> 0.1"
cpub_blocks = "~> 0.1"
cpub_characters = "~> 0.1"
# cpub_emails = "~> 0.1"
cpub_local_auth = "~> 0.1"
cpub_profiles = "~> 0.1"
cpub_users = "~> 0.1"
# cpub_circles, "~> 0.1"
# cpub_comments, "~> 0.1"
# cpub_communities, "~> 0.1"
ok = "~> 2.3.0"

View file

@ -1,58 +1,187 @@
defmodule VoxPublica.Accounts do
use OK.Pipe
alias CommonsPub.Accounts.Account
alias CommonsPub.Emails.Email
alias Ecto.Changeset
alias Pointers.Changesets
alias VoxPublica.Accounts.{LoginForm, RegisterForm}
alias VoxPublica.Repo
alias VoxPublica.Accounts.{
Emails,
ChangeEmailForm,
ConfirmEmailForm,
LoginForm,
ResetPasswordForm,
SignupForm,
}
alias VoxPublica.{Mailer, Repo, Utils}
import Ecto.Query
def register(attrs) do
changeset = RegisterForm.changeset(attrs)
with {:ok, form} <- Changeset.apply_action(changeset, :insert),
{:error, _} <- Repo.put(create_changeset(Map.from_struct(form))) do
{:error, :taken}
end
end
def get_for_session(id) when is_binary(id), do: Repo.get(Account, id)
defp create_changeset(attrs) do
@type changeset_name :: :change_password | :confirm_email | :login | :reset_password | :signup
@spec changeset(changeset_name, attrs :: map) :: Changeset.t
def changeset(:change_password, attrs) when not is_struct(attrs),
do: ChangePassowrdForm.changeset(attrs)
def changeset(:confirm_email, attrs) when not is_struct(attrs),
do: ConfirmEmailForm.changeset(attrs)
def changeset(:login, attrs) when not is_struct(attrs),
do: LoginForm.changeset(attrs)
def changeset(:reset_password, attrs) when not is_struct(attrs),
do: ResetPasswordForm.changeset(attrs)
def changeset(:signup, attrs) when not is_struct(attrs),
do: SignupForm.changeset(attrs)
@doc false
def signup_changeset(%SignupForm{}=form),
do: signup_changeset(Map.from_struct(form))
def signup_changeset(attrs) when not is_struct(attrs) do
%Account{email: nil, login_credential: nil}
|> Account.changeset(attrs)
|> Changesets.cast_assoc(:email, attrs)
|> Changesets.cast_assoc(:login_credential, attrs)
end
def login(attrs) do
cs = LoginForm.changeset(attrs)
login_check_attrs(Changeset.apply_action(cs, :insert))
### signup
def signup(attrs) when not is_struct(attrs),
do: signup(changeset(:signup, attrs))
def signup(%Changeset{data: %SignupForm{}}=cs),
do: Changeset.apply_action(cs, :insert) ~>> signup()
def signup(%SignupForm{}=form),
do: signup(signup_changeset(form))
def signup(%Changeset{data: %Account{}}=cs) do
Repo.transact_with fn -> # revert if email send fails
Repo.put(cs)
|> Utils.replace_error(:taken)
~>> send_confirm_email()
end
end
defp login_check_attrs({:error, changeset}), do: {:error, changeset}
defp login_check_attrs({:ok, form}) do
find_for_login_query(form.email)
|> Repo.one()
|> login_check_password(form)
### login
def login(attrs) when not is_struct(attrs),
do: login(changeset(:login, attrs))
def login(%Changeset{data: %LoginForm{}}=cs) do
with {:ok, form} <- Changeset.apply_action(cs, :insert) do
form
|> find_by_email_query()
|> Repo.single()
~>> check_password(form)
~>> check_confirmed()
end
end
defp login_check_password(nil, _form) do
defp check_password(nil, _form) do
Argon2.no_user_verify()
{:error, :no_match}
end
defp login_check_password(account, form) do
defp check_password(account, form) do
if Argon2.verify_pass(form.password, account.login_credential.password_hash),
do: login_check_confirmed(account),
do: {:ok, account},
else: {:error, :no_match}
end
defp login_check_confirmed(%Account{email: %{email_confirmed_at: nil}}),
defp check_confirmed(%Account{email: %{confirmed_at: nil}}),
do: {:error, :email_not_confirmed}
defp login_check_confirmed(%Account{email: %{email_confirmed_at: _}}=account),
defp check_confirmed(%Account{email: %{confirmed_at: _}}=account),
do: {:ok, account}
defp find_for_login_query(email) when is_binary(email) do
### request_confirm_email
def request_confirm_email(params) when not is_struct(params),
do: request_confirm_email(changeset(:confirm_email, params))
def request_confirm_email(%Changeset{data: %ConfirmEmailForm{}}=cs),
do: Changeset.apply_action(cs, :insert) ~>> request_confirm_email()
def request_confirm_email(%ConfirmEmailForm{}=form) do
case Repo.one(find_by_email_query(form.email)) do
nil -> {:error, :not_found}
%Account{email: email}=account -> request_confirm_email(account)
end
end
def request_confirm_email(%Account{email: %{}=email}=account) do
cond do
not is_nil(email.confirmed_at) -> {:error, :confirmed}
# why not refresh here? it provides a window of DOS opportunity
# against a user completing their activation.
DateTime.utc_now() < email.confirm_until ->
with {:ok, _} <- Mailer.send_now(Emails.confirm_email(account), email.email),
do: {:ok, :resent, account}
true ->
account = refresh_confirm_email_token(account)
with {:ok, _} <- send_confirm_email(Emails.confirm_email(account)),
do: {:ok, :refreshed, account}
end
end
defp refresh_confirm_email_token(%Account{email: %Email{}=email}=account) do
with {:ok, email} <- Repo.update(Email.put_token(email)),
do: {:ok, %{ account | email: email }}
end
### confirm_email
def confirm_email(%Account{}=account) do
with {:ok, email} <- Repo.update(Email.confirm(account.email)),
do: {:ok, %{ account | email: email } }
end
def confirm_email(token) when is_binary(token) do
Repo.transact_with fn ->
case Repo.one(find_for_confirm_email_query(token)) do
nil -> {:error, :not_found}
%Account{email: %Email{}=email} = account ->
cond do
not is_nil(email.confirmed_at) -> {:error, :confirmed, account}
is_nil(email.confirm_until) -> {:error, :no_expiry, account}
DateTime.utc_now() < email.confirm_until -> confirm_email(account)
true -> {:error, :expired, account}
end
end
end
end
defp send_confirm_email(%Account{}=account) do
case Mailer.send_now(Emails.confirm_email(account), account.email.email) do
{:ok, _mail} -> {:ok, account}
_ -> {:error, :email}
end
end
### queries
# defp get_for_session_query(token) when is_binary(token) do
# from a in Account,
# join: e in assoc(a, :email),
# where: e.confirm_token == ^token,
# preload: [email: e]
# end
defp find_for_confirm_email_query(token) when is_binary(token) do
from a in Account,
join: e in assoc(a, :email),
where: e.confirm_token == ^token,
preload: [email: e]
end
defp find_by_email_query(%{email: email}), do: find_by_email_query(email)
defp find_by_email_query(email) when is_binary(email) do
from a in Account,
join: e in assoc(a, :email),
join: lc in assoc(a, :login_credential),
@ -60,30 +189,4 @@ defmodule VoxPublica.Accounts do
preload: [email: e, login_credential: lc]
end
def confirm_email(%Account{}=account) do
Repo.transact_with fn ->
account = Repo.preload(account, :email)
with {:ok, email} <- Repo.update(Email.confirm(account.email)),
do: {:ok, %{ account | email: email } }
end
end
def confirm_email(token) when is_binary(token) do
Repo.transact_with fn ->
case Repo.one(find_for_confirm_email_query(token)) do
nil -> {:error, :not_found}
%Account{email: %Email{}=email}=account ->
with {:ok, email} <- Repo.update(Email.confirm(email)),
do: {:ok, %{ account | email: email } }
end
end
end
defp find_for_confirm_email_query(token) when is_binary(token) do
from a in Account,
join: e in assoc(a, :email),
where: e.email_token == ^token,
preload: [email: e]
end
end

View file

@ -0,0 +1,24 @@
defmodule VoxPublica.Accounts.ChangePasswordForm do
use Ecto.Schema
alias Ecto.Changeset
alias VoxPublica.Accounts.ChangePasswordForm
embedded_schema do
field :old_password, :string
field :password, :string
field :password_confirmation, :string
end
@cast [:old_password, :password, :password_confirmation]
@required @cast
def changeset(form \\ %ChangePasswordForm{}, attrs) do
form
|> Changeset.cast(attrs, @cast)
|> Changeset.validate_required(@required)
|> Changeset.validate_length(:password, min: 10, max: 64)
|> Changeset.validate_confirmation(:password)
end
end

View file

@ -0,0 +1,21 @@
defmodule VoxPublica.Accounts.ConfirmEmailForm do
use Ecto.Schema
alias Ecto.Changeset
alias VoxPublica.Accounts.ConfirmEmailForm
embedded_schema do
field :email, :string
end
@cast [:email]
@required @cast
def changeset(form \\ %ConfirmEmailForm{}, attrs) do
form
|> Changeset.cast(attrs, @cast)
|> Changeset.validate_required(@required)
|> Changeset.validate_format(:email, ~r(^[^@]{1,128}@[^@\.]+\.[^@]{2,128}$))
end
end

29
lib/accounts/emails.ex Normal file
View file

@ -0,0 +1,29 @@
defmodule VoxPublica.Accounts.Emails do
import Bamboo.Email
import Bamboo.Phoenix
alias CommonsPub.Accounts.Account
alias Pointers.Changesets
alias VoxPublica.Web.EmailView
def confirm_email(%Account{email: %{email: email}}=account) when is_binary(email) do
conf =
Application.get_env(:vox_publica, __MODULE__, [])
|> Keyword.get(:confirm_email, [])
new_email()
|> subject(Keyword.get(conf, :subject, "Confirm your email - VoxPublica"))
|> put_html_layout({EmailView, "confirm_email.html"})
|> put_text_layout({EmailView, "confirm_email.text"})
end
def reset_password(%Account{email: %{email: email}}=account) when is_binary(email) do
conf =
Application.get_env(:vox_publica, __MODULE__, [])
|> Keyword.get(:reset_password_email, [])
new_email()
|> subject(Keyword.get(conf, :subject, "Reset your password - VoxPublica"))
|> put_html_layout({EmailView, "reset_password.html"})
|> put_text_layout({EmailView, "reset_password.text"})
end
end

View file

@ -0,0 +1,23 @@
defmodule VoxPublica.Accounts.ResetPasswordForm do
use Ecto.Schema
alias Ecto.Changeset
alias VoxPublica.Accounts.ResetPasswordForm
embedded_schema do
field :password, :string
field :password_confirmation, :string
end
@cast [:password, :password_confirmation]
@required @cast
def changeset(form \\ %ResetPasswordForm{}, attrs) do
form
|> Changeset.cast(attrs, @cast)
|> Changeset.validate_required(@required)
|> Changeset.validate_length(:password, min: 10, max: 64)
|> Changeset.validate_confirmation(:password)
end
end

View file

@ -1,11 +1,10 @@
defmodule VoxPublica.Accounts.RegisterForm do
defmodule VoxPublica.Accounts.SignupForm do
use Ecto.Schema
alias Ecto.Changeset
alias VoxPublica.Accounts.RegisterForm
alias VoxPublica.Accounts.SignupForm
embedded_schema do
field :form, :string, virtual: true
field :email, :string
field :password, :string
end
@ -13,12 +12,12 @@ defmodule VoxPublica.Accounts.RegisterForm do
@cast [:email, :password]
@required @cast
def changeset(form \\ %RegisterForm{}, attrs) do
def changeset(form \\ %SignupForm{}, attrs) do
form
|> Changeset.cast(attrs, @cast)
|> Changeset.validate_required(@required)
|> Changeset.validate_format(:email, ~r(^[^@]{1,128}@[^@\.]+\.[^@]{2,128}$))
|> Changeset.validate_length(:password, min: 10)
|> Changeset.validate_length(:password, min: 10, max: 64)
end
end

View file

@ -37,7 +37,7 @@ defmodule VoxPublica.ActivityPub.Adapter do
actor <- format_actor(user) do
{:ok, actor}
else
_ -> {:error, "not found"}
_ -> {:error, :not_found}
end
end

View file

@ -1,9 +1,10 @@
defmodule VoxPublica.Fake do
def email, do: Faker.Internet.email()
def confirm_token, do: Base.encode32(Faker.random_bytes(10), pad: false)
# def location, do: Faker.Pokemon.location()
def name, do: Faker.Person.name()
def password, do: Base.encode64(Faker.random_bytes(10), pad: false)
def password, do: Base.encode32(Faker.random_bytes(10), pad: false)
def summary, do: Faker.Lorem.sentence(6..15)
def username, do: String.replace(Faker.Internet.user_name(), ~r/\./, "_")
def website, do: Faker.Internet.domain_name()
@ -43,5 +44,6 @@ defmodule VoxPublica.Fake do
|> Map.put_new_lazy(:icon_url, &icon_url/0)
|> Map.put_new_lazy(:image_url, &image_url/0)
|> Map.put_new(:is_followed, false)
|> Map.put_new(:is_instance_admin, true)
end
end

31
lib/mailer.ex Normal file
View file

@ -0,0 +1,31 @@
defmodule VoxPublica.Mailer do
use Bamboo.Mailer, otp_app: :vox_publica
alias Bamboo.Email
require Logger
def send_now(email, to) do
from =
Application.get_env(:vox_publica, __MODULE__, [])
|> Keyword.get(:from_address, "noreply@voxpub.local")
try do
mail =
email
|> Email.from(from)
|> Email.to(to)
deliver_now(mail)
{:ok, mail}
rescue
error in Bamboo.SMTPAdapter.SMTPError ->
# le sigh, i give up
Logger.error("Email delivery error: #{inspect(error.raw)}")
{:error, error}
# case error.raw do
# {:no_credentials, _} -> {:error, :config}
# {:retries_exceeded, _} -> {:error, :rejected}
# # give up
# _ -> raise error
# end
end
end
end

View file

@ -22,14 +22,7 @@ defmodule VoxPublica.Repo do
"""
def put(%Changeset{}=changeset) do
with {:error, changeset} <- insert(changeset) do
changes = Enum.reduce(changeset.changes, changeset.changes, fn {k, v}, acc ->
case v do
%Changeset{valid?: false} ->
Map.put(acc, k, Changesets.rewrite_child_errors(v))
_ -> acc
end
end)
{:error, %{ changeset | changes: changes }}
Changesets.rewrite_constraint_errors(changeset)
end
end
@ -52,12 +45,10 @@ defmodule VoxPublica.Repo do
@doc """
Like Repo.one, but returns an ok/error tuple.
"""
def single(q) do
case one(q) do
nil -> {:error, "not found"}
other -> {:ok, other}
end
end
def single(q), do: single2(one(q))
defp single2(nil), do: {:error, :not_found}
defp single2(other), do: {:ok, other}
end

View file

@ -1,45 +0,0 @@
defmodule VoxPublica.Users do
@doc """
A User is a logical identity within the system belonging to an Account.
"""
alias CommonsPub.Accounts.Account
alias CommonsPub.Users.User
alias Pointers.Changesets
alias VoxPublica.Repo
import Ecto.Query
def create(%Account{id: id}, attrs),
do: Repo.put(changeset(Map.put(attrs, :account_id, id)))
def update(%User{} = user, attrs), do: Repo.update(changeset(user, attrs))
def changeset(user \\ %User{}, attrs) do
User.changeset(user, attrs)
|> Changesets.cast_assoc(:accounted, attrs)
|> Changesets.cast_assoc(:character, attrs)
|> Changesets.cast_assoc(:profile, attrs)
|> Changesets.cast_assoc(:actor, attrs)
end
def by_account(%Account{}=account), do: Repo.all(by_account_query(account))
def by_account_query(%Account{id: account_id}) do
from u in User,
join: a in assoc(u, :accounted),
join: c in assoc(u, :character),
where: a.account_id == ^account_id,
preload: [accounted: a, character: c]
end
def by_username(username), do: Repo.single(by_username_query(username))
def by_username_query(username) do
from u in User,
join: p in assoc(u, :profile),
join: c in assoc(u, :character),
join: a in assoc(u, :actor),
join: ac in assoc(u, :accounted),
where: c.username == ^username,
preload: [profile: p, character: c, actor: a, accounted: ac]
end
end

32
lib/users/create_form.ex Normal file
View file

@ -0,0 +1,32 @@
defmodule VoxPublica.Users.CreateForm do
use Ecto.Schema
alias Ecto.Changeset
alias CommonsPub.Accounts.Account
alias VoxPublica.Users.CreateForm
embedded_schema do
field :username, :string
field :name, :string
field :summary, :string
field :account_id, :integer
end
@cast [:username, :name, :summary]
@required @cast
# @defaults [
# cast: [:username, :name, :summary],
# required: [:username, :name, :summary],
# ]
def changeset(form \\ %CreateForm{}, attrs, %Account{id: id}) do
form
|> Changeset.cast(attrs, @cast)
|> Changeset.change(account_id: id)
|> Changeset.validate_required(@required)
|> Changeset.validate_format(:username, ~r(^[a-z][a-z0-9_]{2,30}$)i)
|> Changeset.validate_length(:name, min: 3, max: 50)
|> Changeset.validate_length(:summary, min: 20, max: 500)
end
end

85
lib/users/users.ex Normal file
View file

@ -0,0 +1,85 @@
defmodule VoxPublica.Users do
@doc """
A User is a logical identity within the system belonging to an Account.
"""
use OK.Pipe
alias CommonsPub.Accounts.Account
alias CommonsPub.Users.User
alias VoxPublica.Users.CreateForm
alias Pointers.Changesets
alias VoxPublica.{Repo, Utils}
alias Ecto.Changeset
import Ecto.Query
@type changeset_name :: :create
@spec changeset(changeset_name, attrs :: map, %Account{}) :: Changeset.t
def changeset(:create, attrs, %Account{}=account), do: CreateForm.changeset(attrs, account)
def create(attrs, %Account{}=account) when not is_struct(attrs),
do: create(changeset(:create, attrs, account))
defp create(%Changeset{data: %CreateForm{}}=cs),
do: Changeset.apply_action(cs, :insert) ~>> create()
defp create(%CreateForm{}=form) do
Map.from_struct(form)
|> create_changeset()
|> Repo.put()
end
def update(%User{} = user, attrs), do: Repo.update(create_changeset(user, attrs))
def create_changeset(user \\ %User{}, attrs) do
User.changeset(user, attrs)
|> Changesets.cast_assoc(:accounted, attrs)
|> Changesets.cast_assoc(:character, attrs)
|> Changesets.cast_assoc(:profile, attrs)
|> Changesets.cast_assoc(:actor, attrs)
end
def by_account(%Account{id: id}), do: by_account(id)
def by_account(account_id) when is_binary(account_id),
do: Repo.all(by_account_query(account_id))
def by_account_query(account_id) do
from u in User,
join: a in assoc(u, :accounted),
join: c in assoc(u, :character),
where: a.account_id == ^account_id,
preload: [accounted: a, character: c]
end
def by_username(username), do: Repo.single(by_username_query(username))
def by_username_query(username) do
from u in User,
join: p in assoc(u, :profile),
join: c in assoc(u, :character),
join: a in assoc(u, :actor),
join: ac in assoc(u, :accounted),
where: c.username == ^username,
preload: [profile: p, character: c, actor: a, accounted: ac]
end
def for_switch_user(username, account_id) do
Repo.single(for_switch_user_query(username))
~>> check_account_id(account_id)
end
def check_account_id(%User{}=user, account_id) do
if user.accounted.account_id == account_id,
do: {:ok, user},
else: {:error, :not_permitted}
end
def for_switch_user_query(username) do
from u in User,
join: c in assoc(u, :character),
join: a in assoc(u, :accounted),
where: c.username == ^username,
preload: [character: c, accounted: a],
order_by: [asc: u.id]
end
end

12
lib/utils.ex Normal file
View file

@ -0,0 +1,12 @@
defmodule VoxPublica.Utils do
def map_error({:error, value}, fun), do: fun.(value)
def map_error(other, _), do: other
def replace_error({:error, _}, value), do: {:error, value}
def replace_error(other, _), do: other
def replace_nil(nil, value), do: value
def replace_nil(other, _), do: other
end

215
lib/web/common_helper.ex Normal file
View file

@ -0,0 +1,215 @@
defmodule VoxPublica.Web.CommonHelper do
import Phoenix.LiveView
require Logger
alias CommonsPub.Web.GraphQL.LikesResolver
alias VoxPublica.Fake
def strlen(x) when is_nil(x), do: 0
def strlen(%{} = obj) when obj == %{}, do: 0
def strlen(%{}), do: 1
def strlen(x) when is_binary(x), do: String.length(x)
def strlen(x) when is_list(x), do: length(x)
def strlen(x) when x > 0, do: 1
# let's say that 0 is nothing
def strlen(x) when x == 0, do: 0
@doc "Returns a value, or a fallback if not present"
def e(key, fallback) do
if(strlen(key) > 0) do
key
else
fallback
end
end
@doc "Returns a value from a map, or a fallback if not present"
def e(map, key, fallback) do
if(is_map(map)) do
# attempt using key as atom or string
map_get(map, key, fallback)
else
fallback
end
end
@doc "Returns a value from a nested map, or a fallback if not present"
def e(map, key1, key2, fallback) do
e(e(map, key1, %{}), key2, fallback)
end
def e(map, key1, key2, key3, fallback) do
e(e(map, key1, key2, %{}), key3, fallback)
end
def e(map, key1, key2, key3, key4, fallback) do
e(e(map, key1, key2, key3, %{}), key4, fallback)
end
def is_numeric(str) do
case Float.parse(str) do
{_num, ""} -> true
_ -> false
end
end
def to_number(str) do
case Float.parse(str) do
{num, ""} -> num
_ -> 0
end
end
@doc """
Attempt geting a value out of a map by atom key, or try with string key, or return a fallback
"""
def map_get(map, key, fallback) when is_atom(key) do
Map.get(map, key, map_get(map, Atom.to_string(key), fallback))
end
@doc """
Attempt geting a value out of a map by string key, or try with atom key (if it's an existing atom), or return a fallback
"""
def map_get(map, key, fallback) when is_binary(key) do
Map.get(
map,
key,
Map.get(
map,
Recase.to_camel(key),
Map.get(
map,
maybe_str_to_atom(key),
Map.get(
map,
maybe_str_to_atom(Recase.to_camel(key)),
fallback
)
)
)
)
end
def map_get(map, key, fallback) do
Map.get(map, key, fallback)
end
def maybe_str_to_atom(str) do
try do
String.to_existing_atom(str)
rescue
ArgumentError -> str
end
end
def input_to_atoms(data) do
data |> Map.new(fn {k, v} -> {maybe_str_to_atom(k), v} end)
end
def random_string(length) do
:crypto.strong_rand_bytes(length) |> Base.url_encode64() |> binary_part(0, length)
end
def r(html), do: Phoenix.HTML.raw(html)
def markdown(html), do: r(markdown_to_html(html))
def markdown_to_html(nil) do
nil
end
def markdown_to_html(content) do
content
|> Earmark.as_html!()
|> external_links()
end
# open outside links in a new tab
def external_links(content) do
Regex.replace(~r/(<a href=\"http.+\")>/U, content, "\\1 target=\"_blank\">")
end
def date_from_now(date) do
with {:ok, from_now} <-
Timex.shift(date, minutes: -3)
|> Timex.format("{relative}", :relative) do
from_now
else
_ ->
""
end
end
@doc """
This initializes the socket assigns
"""
def init_assigns(
_params,
%{
"auth_token" => auth_token,
"current_user" => current_user,
"_csrf_token" => csrf_token
} = _session,
%Phoenix.LiveView.Socket{} = socket
) do
# Logger.info(session_preloaded: session)
socket
|> assign(:auth_token, fn -> auth_token end)
|> assign(:current_user, fn -> current_user end)
|> assign(:csrf_token, fn -> csrf_token end)
|> assign(:static_changed, static_changed?(socket))
|> assign(:search, "")
end
def init_assigns(
_params,
%{
"auth_token" => auth_token,
"_csrf_token" => csrf_token
} = session,
%Phoenix.LiveView.Socket{} = socket
) do
# Logger.info(session_load: session)
current_user = Fake.user_live()
socket
|> assign(:csrf_token, csrf_token)
|> assign(:static_changed, static_changed?(socket))
|> assign(:auth_token, auth_token)
|> assign(:show_title, false)
|> assign(:toggle_post, false)
|> assign(:current_context, nil)
|> assign(:current_user, current_user)
|> assign(:search, "")
end
def init_assigns(
_params,
%{
"_csrf_token" => csrf_token
} = _session,
%Phoenix.LiveView.Socket{} = socket
) do
socket
|> assign(:csrf_token, csrf_token)
|> assign(:static_changed, static_changed?(socket))
|> assign(:current_user, nil)
|> assign(:search, "")
end
def init_assigns(_params, _session, %Phoenix.LiveView.Socket{} = socket) do
socket
|> assign(:current_user, nil)
|> assign(:search, "")
|> assign(:static_changed, static_changed?(socket))
end
def paginate_next(fetch_function, %{assigns: assigns} = socket) do
{:noreply, socket |> assign(page: assigns.page + 1) |> fetch_function.(assigns)}
end
end

View file

@ -0,0 +1,12 @@
defmodule VoxPublica.Web.ChangePasswordController do
use VoxPublica.Web, :controller
plug MustLogIn, load_account: true
def index(conn, _) do
end
def create(conn, _) do
end
end

View file

@ -0,0 +1,53 @@
defmodule VoxPublica.Web.ConfirmEmailController do
use VoxPublica.Web, :controller
alias VoxPublica.Accounts
plug MustBeGuest
def index(conn, _),
do: render(conn, "form.html", requested: false, error: nil, form: form())
def show(conn, %{"id" => token}) do
case Accounts.confirm_email(token) do
{:ok, account} ->
confirmed(conn, account)
{:error, :confirmed, _} ->
already_confirmed(conn)
{:error, :expired, _} ->
render(conn, "form.html", requested: false, error: :expired_link, form: form())
_ ->
render(conn, "form.html", requested: false, error: :not_found, form: form())
end
end
def create(conn, params) do
form = Map.get(params, "confirm_email_form", %{})
case Accounts.request_confirm_email(form(form)) do
{:ok, _, _} ->
render(conn, "form.html", requested: true, error: nil, form: form())
{:error, :confirmed} ->
already_confirmed(conn)
{:error, :not_found} ->
render(conn, "form.html", requested: false, error: :not_found, form: form())
{:error, changeset} ->
render(conn, "form.html", requested: false, error: nil, form: changeset)
end
end
defp form(params \\ %{}), do: Accounts.changeset(:confirm_email, params)
defp confirmed(conn, account) do
conn
|> put_session(:account_id, account.id)
|> put_flash(:info, "Welcome back! Thanks for confirming your email address.")
|> redirect(to: "/home")
end
defp already_confirmed(conn) do
conn
|> put_flash(:info, "You've already confirmed your email address. You can log in now.")
|> redirect(to: "/login")
end
end

View file

@ -0,0 +1,31 @@
defmodule VoxPublica.Web.CreateUserController do
use VoxPublica.Web, :controller
alias CommonsPub.Users.User
alias VoxPublica.Web.Plugs.MustLogIn
alias VoxPublica.Users
plug MustLogIn, load_account: true
def index(conn, _),
do: render(conn, "form.html", form: form(conn.assigns[:account]))
def create(conn, params) do
Map.get(params, "create_form", %{})
|> Users.create(conn.assigns[:account])
|> case do
{:ok, user} -> switched(conn, user)
{:error, form} ->
render(conn, "form.html", form: form)
end
end
defp form(attrs \\ %{}, account), do: Users.changeset(:create, attrs, account)
defp switched(conn, %User{id: id, character: %{username: username}}) do
conn
|> put_flash(:info, "Welcome, #{username}, you're all ready to go!")
|> put_session(:user_id, id)
|> redirect(to: "/home/@#{username}")
end
end

View file

@ -0,0 +1,12 @@
defmodule VoxPublica.Web.ForgotPasswordController do
use VoxPublica.Web, :controller
plug MustBeGuest
def index(conn, _) do
end
def create(conn, _) do
end
end

View file

@ -0,0 +1,30 @@
defmodule VoxPublica.Web.LoginController do
use VoxPublica.Web, :controller
alias VoxPublica.Accounts
plug MustBeGuest
def index(conn, _), do: render(conn, "form.html", error: nil, form: form())
def create(conn, params) do
form = Map.get(params, "login_form", %{})
case Accounts.login(Accounts.changeset(:login, form)) do
{:ok, account} ->
logged_in(account, conn)
{:error, error} when is_atom(error) ->
render(conn, "form.html", error: error, form: form())
{:error, changeset} ->
render(conn, "form.html", error: nil, form: changeset)
end
end
defp form(params \\ %{}), do: Accounts.changeset(:login, params)
defp logged_in(account, conn) do
conn
|> put_session(:account_id, account.id)
|> redirect(to: "/home")
end
end

View file

@ -0,0 +1,12 @@
defmodule VoxPublica.Web.ResetPasswordController do
use VoxPublica.Web, :controller
plug MustBeGuest
def index(conn, %{"token" => token}) do
end
def create(conn, %{"token" => token}) do
end
end

View file

@ -0,0 +1,30 @@
defmodule VoxPublica.Web.SignupController do
use VoxPublica.Web, :controller
alias VoxPublica.Accounts
plug MustBeGuest
def index(conn, _) do
if get_session(conn, :account_id),
do: redirect(conn, to: "/home"),
else: render(conn, "form.html", registered: false, error: nil, form: form())
end
def create(conn, params) do
if get_session(conn, :account_id) do
redirect(conn, to: "/home")
else
case Accounts.signup(Map.get(params, "signup_form", %{})) do
{:ok, _account} ->
render(conn, "form.html", registered: true)
{:error, :taken} ->
render(conn, "form.html", registered: false, error: :taken, form: form())
{:error, changeset} ->
render(conn, "form.html", registered: false, error: nil, form: changeset)
end
end
end
defp form(params \\ %{}), do: Accounts.changeset(:signup, params)
end

View file

@ -0,0 +1,61 @@
defmodule VoxPublica.Web.SwitchUserController do
use VoxPublica.Web, :controller
alias VoxPublica.Users
plug MustLogIn, load_account: true
def index(conn, _) do
case Users.by_account(conn.assigns[:account]) do
[] -> no_users(conn)
users -> list(conn, users)
end
end
def list(conn, users), do: render(conn, "list.html", users: users)
def show(conn, %{"username" => username}),
do: show(get_session(conn, :account_id), username, conn)
defp show(nil, _username, conn), do: not_logged_in(conn)
defp show(account_id, username, conn), do: lookup(account_id, username, conn)
defp lookup(account_id, username, conn),
do: lookup(Users.for_switch_user(username, account_id), conn)
defp lookup({:ok, user}, conn), do: switch(conn, user)
defp lookup({:error, :not_found}, conn), do: not_found(conn)
defp lookup({:error, :not_permitted}, conn), do: not_permitted(conn)
defp switch(conn, user) do
conn
|> put_session(:user_id, user.id)
|> put_flash(:info, "Welcome back, @#{user.character.username}!")
|> redirect(to: "/home/@#{user.character.username}")
end
defp no_users(conn) do
conn
|> put_flash(:info, "Hey there! Let's fill out your profile!")
|> redirect(to: "/create-user")
end
defp not_logged_in(conn) do
conn
|> put_flash(:error, "You must log in to switch user.")
|> redirect(to: "/login")
end
defp not_found(conn) do
conn
|> put_flash(:error, "This username does not exist.")
|> redirect(to: "/switch-user")
end
defp not_permitted(conn) do
conn
|> put_flash(:error, "You are not permitted to switch to this user.")
|> redirect(to: "/switch-user")
end
end

View file

@ -1,24 +0,0 @@
defmodule VoxPublica.Web.ConfirmEmailLive do
use VoxPublica.Web, :live_view
import VoxPublica.Web.ErrorHelpers
use Phoenix.HTML
alias VoxPublica.Accounts
@impl true
def mount(_params, _session, socket) do
account = Accounts.changeset(%{})
{:ok, assign(socket, changeset: account)}
end
@impl true
def handle_event("validate", _params, socket) do
{:noreply, socket} #assign(socket, results: search(query), query: query)}
end
@impl true
def handle_event("submit", _params, socket) do
{:noreply, socket} #assign(socket, results: search(query), query: query)}
end
end

View file

@ -1,40 +0,0 @@
defmodule VoxPublica.Web.CreateUserLive do
use VoxPublica.Web, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, query: "", results: %{})}
end
# @impl true
# def handle_event("suggest", %{"q" => query}, socket) do
# {:noreply, assign(socket, results: search(query), query: query)}
# end
# @impl true
# def handle_event("register", %{"q" => query}, socket) do
# case search(query) do
# %{^query => vsn} ->
# {:noreply, redirect(socket, external: "https://hexdocs.pm/#{query}/#{vsn}")}
# _ ->
# {:noreply,
# socket
# |> put_flash(:error, "No dependencies found matching \"#{query}\"")
# |> assign(results: %{}, query: query)}
# end
# end
# defp search(query) do
# if not VoxPublica.Web.Endpoint.config(:code_reloader) do
# raise "action disabled when not in development"
# end
# for {app, desc, vsn} <- Application.started_applications(),
# app = to_string(app),
# String.starts_with?(app, query) and not List.starts_with?(desc, ~c"ERTS"),
# into: %{},
# do: {app, vsn}
# end
end

View file

@ -1,7 +0,0 @@
<form>
<label>
Username: <input type="text" />
Password: <input type="text" />
<button type="submit" phx-click="register">Register</button>
</label>
</form>

View file

@ -0,0 +1,34 @@
defmodule VoxPublica.Web.HomeLive do
use VoxPublica.Web, :live_view
import VoxPublica.Web.CommonHelper
def mount(params, session, socket) do
socket = init_assigns(params, session, socket)
{:ok, socket
|> assign(
query: "",
results: %{},
selected_tab: "timeline",
page_title: "My VoxPub"
)}
end
def handle_params(%{"tab" => tab}, _url, socket) do
{:noreply, assign(socket, selected_tab: tab)}
end
def handle_params(_, _url, socket) do
{:noreply, assign(socket, selected_tab: "timeline")}
end
defp link_body(name, icon) do
assigns = %{name: name, icon: icon}
~L"""
<i class="<%= @icon %>"></i>
<%= @name %>
"""
end
end

View file

@ -0,0 +1,25 @@
<div class="page__mainContent">
<div class="my">
<div class="my__hero">
<h1><%=@page_title%></h1>
</div>
<div class="mainContent__navigation home__navigation">
<%= live_patch link_body("My Timeline","feather-activity"),
to: "/",
class: if @selected_tab == "timeline", do: "navigation__item active", else: "navigation__item"
%>
<%= live_patch link_body("My circles", "feather-share-2"),
to: "/circles",
class: if @selected_tab == "circles", do: "navigation__item active", else: "navigation__item"
%>
</div>
<div class="mainContent__selected">
<%= cond do %>
<% @selected_tab == "circles" -> %>
<% true -> %>
timeline
<% end %>
</div>
</div>
</div>

View file

@ -0,0 +1,34 @@
defmodule VoxPublica.Web.IndexLive do
use VoxPublica.Web, :live_view
import VoxPublica.Web.CommonHelper
def mount(params, session, socket) do
socket = init_assigns(params, session, socket)
{:ok, socket
|> assign(
query: "",
results: %{},
selected_tab: "timeline",
page_title: "My VoxPub"
)}
end
def handle_params(%{"tab" => tab}, _url, socket) do
{:noreply, assign(socket, selected_tab: tab)}
end
def handle_params(_, _url, socket) do
{:noreply, assign(socket, selected_tab: "timeline")}
end
defp link_body(name, icon) do
assigns = %{name: name, icon: icon}
~L"""
<i class="<%= @icon %>"></i>
<%= @name %>
"""
end
end

View file

@ -0,0 +1,25 @@
<div class="page__mainContent">
<div class="my">
<div class="my__hero">
<h1><%=@page_title%></h1>
</div>
<div class="mainContent__navigation home__navigation">
<%= live_patch link_body("My Timeline","feather-activity"),
to: "/",
class: if @selected_tab == "timeline", do: "navigation__item active", else: "navigation__item"
%>
<%= live_patch link_body("My circles", "feather-share-2"),
to: "/circles",
class: if @selected_tab == "circles", do: "navigation__item active", else: "navigation__item"
%>
</div>
<div class="mainContent__selected">
<%= cond do %>
<% @selected_tab == "circles" -> %>
<% true -> %>
timeline
<% end %>
</div>
</div>
</div>

View file

@ -1,39 +0,0 @@
defmodule VoxPublica.Web.IndexLive do
use VoxPublica.Web, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, query: "", results: %{})}
end
@impl true
def handle_event("suggest", %{"q" => query}, socket) do
{:noreply, assign(socket, results: search(query), query: query)}
end
@impl true
def handle_event("search", %{"q" => query}, socket) do
case search(query) do
%{^query => vsn} ->
{:noreply, redirect(socket, external: "https://hexdocs.pm/#{query}/#{vsn}")}
_ ->
{:noreply,
socket
|> put_flash(:error, "No dependencies found matching \"#{query}\"")
|> assign(results: %{}, query: query)}
end
end
defp search(query) do
if not VoxPublica.Web.Endpoint.config(:code_reloader) do
raise "action disabled when not in development"
end
for {app, desc, vsn} <- Application.started_applications(),
app = to_string(app),
String.starts_with?(app, query) and not List.starts_with?(desc, ~c"ERTS"),
into: %{},
do: {app, vsn}
end
end

View file

@ -1 +0,0 @@
Home.

View file

@ -1,27 +1,36 @@
@mixin auth-grid() {
display: grid;
grid-template-columns: 3fr 2fr;
grid-template-columns: 1fr 1fr;
margin: 0 auto;
margin-top: 80px;
max-width: 980px;
width: 100%;
grid-template-rows: auto auto;
grid-column-gap: var(--m4);
}
@mixin grid-background() {
height: 100vh;
background: url(https://i.pinimg.com/originals/82/96/9f/82969f3ab96d90482a6354ae4dd8b629.png);
background-size: cover;
background-repeat: no-repeat;
background-position: center;
height: 460px;
}
@mixin grid-form() {
max-width: 600px;
min-width: 320px;
margin: 0 auto;
margin-top: 80px;
border-radius: 6px;
border-radius: 6px;
max-width: 420px;
width: 100%;
h1 {
margin-top: 0;
}
form {
margin-bottom: var(--m3);
.form__container {
margin-bottom: var(--m3)
}
input {
display: block;
width: 100%;
@ -50,17 +59,17 @@
text-decoration: underline;
}
p {
color: var(--color-text-subtle);
color: var(--color-text);
font-size: 14px;
}
}
@mixin auth-wrapper() {
max-width: 320px;
max-width: 420px;
margin: 0 auto;
margin-top: var(--m4);
background-color: var(--color-surface);
border: var(--border);
background: var(--color-background-1);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
border-radius: 6px;
padding: var(--m3);
& > div {
@ -75,6 +84,17 @@
width: 100%;
margin-top: var(--m2)
}
textarea {
background-color: var(--color-surface);
border: var(--border);
border-radius: 4px;
text-indent: 8px;
color: var(--color-text);
width: 100%;
margin-top: 8px;
max-height: 150px;
height: 120px;
}
button {
width: 100%;
margin-top: var(--m2)
@ -100,14 +120,63 @@
}
}
.auth__info {
border-radius: 6px;
background-color: var(--color-foreground-2);
padding: var(--m3);
height: max-content;
.grid__background {
margin: -16px;
margin-bottom: 0;
border-radius: 6px 6px 0 0;
}
h2, h3 {
color: var(--color-background-0);
}
h3 {
font-weight: 500;
}
ol {
list-style: none;
padding: 0;
margin: 0;
li {
margin-bottom: var(--m2);
a {
color: var(--color-secondary);
text-decoration: underline;
font-size: 14px;
}
}
}
}
.page__auth-grid {
@include auth-grid();
.form__confirmation {
max-width: 420px;
width: 100%;
background: var(--color-surface);
border: var(--border);
padding: 16px;
border-radius: 6px;
p {
margin: 0;
color: var(--color-text)
}
h2 {
margin-top: 0
}
}
.grid__background {
@include grid-background()
}
.grid__form {
@include grid-form()
@include grid-form();
}
.form__wrapper {
@include auth-wrapper();
@ -118,3 +187,84 @@
}
}
.section__preview {
display: flex;
.preview__form {
margin-left: var(--m3);
input {
border: none;
text-indent: 0;
font-size: 14px;
background: transparent
}
}
.preview__card {
flex:1;
background: var(--color-background);
border-radius: 6px;
margin-bottom: var(--m4);
.card__upload {
cursor: pointer;
input {
display: none;
}
&.icon {
position: absolute;
top: -70px;
left: 50%;
margin-left: -50px;
}
}
.card__bg {
height: 150px;
border-radius: 6px;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.card__bar {
position: relative;
display: flex;
justify-content: flex-start;
align-items: center;
background: var(--color-background);
.bar__icon {
width: 100px;
height: 100px;
border-radius: 100%;
background-size: cover;
border: var(--border);
border-width: 2px;
box-shadow: 0 4px 20px 4px rgba(0,0,0, .4);
}
.bar__meta {
margin-left: var(--m2);
h3 {
margin: 0;
font-size: 16px;
color: var(--color-text);
}
h4 {
font-size: 14px;
}
}
}
}
}
.box__warning {
padding: 8px 16px;
background: var(--color-warning);
border-radius: 6px;
font-size: 14px;
text-align: center;
a {
font-weight: 700;
color: var(--color-background) !important;
text-decoration: underline
}
}

View file

@ -1,30 +0,0 @@
defmodule VoxPublica.Web.LoginLive do
use VoxPublica.Web, :live_view
import VoxPublica.Web.ErrorHelpers
use Phoenix.HTML
alias VoxPublica.Accounts
alias VoxPublica.Accounts.LoginForm
@impl true
def mount(_params, _session, socket) do
if socket.assigns[:account] do
{:ok, push_redirect(socket, to: "/home", replace: true)}
else
{:ok, assign(socket, login_error: nil, changeset: LoginForm.changeset(%{}))}
end
end
@impl true
def handle_event("submit", attrs, socket) do
case Accounts.login(attrs) do
{:ok, account} ->
{:noreply, push_redirect(assign(socket, :account, account), to: "/home")}
{:error, error} when is_atom(error) ->
{:noreply, assign(socket, login_error: error)}
{:error, changeset} ->
{:noreply, assign(socket, changeset: changeset)}
end
end
end

View file

@ -1,31 +0,0 @@
<div class="guest__container">
<div class="page__auth-grid">
<div class="grid__background"></div>
<div class="grid__form">
<h1>Log in</h1>
<%= if @login_error do %>
<div class="error">
<%= case @login_error do %>
<% :no_match -> %>
<span>Your username and/or password are incorrect.</span>
<% :email_not_confirmed -> %>
<span>You must confirm your email address before logging in. Check your email.</span>
<% end %>
</div>
<% end %>
<%= f = form_for @changeset, "#", [id: "login-form", phx_submit: :submit] %>
<%= text_input f, :email, placeholder: "Type your username or email..." %>
<%= error_tag f, :email %>
<%= text_input f, :password, placeholder: "Type your password..." %>
<%= error_tag f, :password %>
<%= submit "Log in" %>
</form>
<div class="auth__helpers">
<p><span>👋 </span>Don't have an account yet? <%= live_redirect to: "/register" do %>Create a new one<% end %></p>
<p><span>🧐 </span>Trouble logging in? <%= live_redirect to: "/password/forgot" do %>reset your password<% end %>
</div>
</div>
</div>
</div>

View file

@ -1,5 +0,0 @@
defmodule VoxPublica.Web.ChangePasswordLive do
use VoxPublica.Web, :live_view
use Phoenix.HTML
end

View file

@ -1,14 +0,0 @@
<div class="page__auth-simple ">
<h1>Generate a new password</h1>
<form method="post" phx-submit="create-password">
<div class="form__container">
<label for="password">New password</label>
<input name="new-password" id="password" type="password" placeholder="Type your password..." />
</div>
<div class="form__container">
<label for="repeat-password">Confirm new password</label>
<input name="confirm-new-password" id="repeat-password" type="password" placeholder="Repeat your password..." />
</div>
<button type="submit">Update</button>
</form>
</div>

View file

@ -1,6 +0,0 @@
defmodule VoxPublica.Web.ResetPasswordLive do
use VoxPublica.Web, :live_view
use Phoenix.HTML
end

View file

@ -4,9 +4,11 @@ defmodule VoxPublica.Web.ProfileLive do
alias VoxPublica.Web.ProfileNavigationLive
alias VoxPublica.Web.ProfileAboutLive
alias VoxPublica.Fake
import VoxPublica.Web.CommonHelper
@impl true
def mount(params, _session, socket) do
def mount(params, session, socket) do
socket = init_assigns(params, session, socket)
{:ok,
socket
|> assign(

View file

@ -1,33 +0,0 @@
defmodule VoxPublica.Web.SignupLive do
use VoxPublica.Web, :live_view
import VoxPublica.Web.ErrorHelpers
use Phoenix.HTML
alias VoxPublica.Accounts
alias VoxPublica.Accounts.RegisterForm
@impl true
def mount(_params, _session, socket) do
if socket.assigns[:account] do
{:ok, push_redirect(socket, to: "/home", replace: true)}
else
{:ok, assign(socket, registered: false, register_error: nil, changeset: RegisterForm.changeset(%{}))}
end
end
@impl true
def handle_event("submit", params, socket) do
IO.inspect(params, label: "test")
case Accounts.register(params) do
{:ok, account} ->
{:noreply, assign(socket, registered: true, register_error: nil)}
{:error, :taken} ->
{:noreply, assign(socket, register_error: :taken)}
{:error, changeset} ->
{:noreply, assign(socket, changeset: changeset)}
end
end
end

View file

@ -1,41 +0,0 @@
<div class="guest__container">
<div class="page__auth-grid">
<div class="grid__background"></div>
<div class="grid__form">
<h1>Create a new account</h1>
<%= if @registered do %>
<div class="info">
<span>
Now we need you to confirm your email address. We've mailed
you a link. Please click it to continue.
</span>
</div>
<% else %>
<%= if @register_error == :taken do %>
<div class="error">
<span>
</div>
<% end %>
<div class="form__wrapper">
<%= f = form_for @changeset, "#", [id: "register-form", phx_submit: :submit] %>
<div class="form__container">
<%= text_input f, :email, placeholder: "Type your email..." %>
<%= error_tag f, :email %>
</div>
<div class="form__container">
<%= password_input f, :password, placeholder: "Type your password..." %>
<%= error_tag f, :password %>
</div>
<%= submit "Sign up" %>
</form>
<div class="auth__helpers">
<p><span>👋 </span>You already have an account? <%= live_redirect to: "/login" do %>Log in<% end %></p>
</div>
</div>
<% end %>
</div>
</div>
</div>

View file

@ -0,0 +1,21 @@
defmodule VoxPublica.Web.Plugs.MustBeGuest do
import Plug.Conn
import Phoenix.Controller, only: [redirect: 2, put_flash: 3]
def init(opts), do: opts
def call(conn, opts) do
if get_session(conn, :account_id),
do: not_permitted(conn),
else: conn
end
defp not_permitted(conn) do
conn
|> put_flash(:error, "That page is only accessible to guests.")
|> redirect(to: "/home")
|> halt()
end
end

View file

@ -0,0 +1,39 @@
defmodule VoxPublica.Web.Plugs.MustLogIn do
import Plug.Conn
import Phoenix.Controller
alias VoxPublica.Accounts
def init(opts), do: opts
def call(conn, opts) do
id = get_session(conn, :account_id)
if id,
do: load(conn, id, opts),
else: not_permitted(conn)
end
defp load(conn, account_id, opts) do
if Keyword.get(opts, :load_account, false),
do: load(conn, Accounts.get_for_session(account_id)),
else: conn
end
defp load(conn, nil) do
conn
|> put_flash(:error, "You must log in to access this page.")
|> delete_session(:account_id)
|> redirect(to: "/login")
|> halt()
end
defp load(conn, account), do: assign(conn, :account, account)
defp not_permitted(conn) do
conn
|> put_flash(:error, "You must log in to access this page.")
|> redirect(to: "/login")
|> halt()
end
end

View file

@ -13,19 +13,23 @@ defmodule VoxPublica.Web.Router do
scope "/", VoxPublica.Web do
pipe_through :browser
# guest visible pages
live "/", IndexLive, :index
live "/register", SignupLive, :register
live "/login", LoginLive, :login
live "/password/forgot", ResetPasswordLive, :reset_password
live "/password/change", ChangePasswordLive, :change_password
live "/password/change/:token", ChangePasswordLive, :change_password_confirm
resources "/signup", SignupController, only: [:index, :create]
resources "/confirm-email", ConfirmEmailController, only: [:index, :show, :create]
resources "/login", LoginController, only: [:index, :create]
resources "/password/forgot", ForgotPasswordController, only: [:index, :create]
resources "/password/reset/:token", ResetPasswordController, only: [:index, :create]
resources "/password/change", ChangePasswordController, only: [:index, :create]
# authenticated pages
resources "/create-user", CreateUserController, only: [:index, :create]
get "/switch-user", SwitchUserController, :index
get "/switch-user/@:username", SwitchUserController, :show
live "/@:username", ProfileLive
live "/@:username/:tab", ProfileLive
# get "/confirm-email/:token", ConfirmEmailController, :confirm_email
# live "/reset-password", ResetPasswordLive, :reset_password
# live "/reset-password/:token", ResetPasswordLive, :reset_password_confirm
# live "/home", HomeLive, :homellow only admins to access it.
live "/home", HomeLive, :home
live "/home/@:username", HomeLive, :home_user
live "/@:username", ProfileLive, :profile
live "/@:username/:tab", ProfileLive, :profile_tab
end
# If your application does not have an admins-only section yet,

View file

@ -0,0 +1,14 @@
<div class="page__auth-simple ">
<h1>Generate a new password</h1>
<%= f = form_for @form, "/password/change", [method: :post] %>
<div class="form__container">
<label for="password">New password</label>
<input name="new-password" id="password" type="password" placeholder="Type your password..." />
</div>
<div class="form__container">
<label for="repeat-password">Confirm new password</label>
<input name="confirm-new-password" id="repeat-password" type="password" placeholder="Repeat your password..." />
</div>
<button type="submit">Update</button>
</form>
</div>

View file

@ -0,0 +1,44 @@
<div class="guest__container">
<div class="page__auth-grid">
<div class="grid__background"></div>
<div class="grid__form">
<%= if @requested do %>
<div class="form__confirmation">
<h2>Great!</h2>
<p>
We've mailed you another link. Please click it to continue.
</p>
</div>
<% else %>
<h1>Re-request email confirmation link</h1>
<%= if @error do %>
<div class="error">
<%= case @error do %>
<% :not_found -> %>
<span>Invalid confirmation link. Please request a new one below.</span>
<% :expired -> %>
<span>This confirmation link has expired. Please request a new one below.</span>
<% end %>
</div>
<% end %>
<%= f = form_for @form, Routes.confirm_email_path(@conn, :index), [id: "confirm-email-form", phx_submit: :submit] %>
<%= text_input f, :email, placeholder: "Type your email..." %>
<%= error_tag f, :email %>
<%= submit "Mail me!" %>
</form>
<div class="auth__helpers">
<p>
<span>&#x01f44b; </span>
Already confirmed your email?
<%= link "Log in", to: Routes.login_path(@conn, :index) %>.
</p>
<p>
<span>&#x01f9d0; </span>
Don&#39;t have an account yet?
<%= link "Create a new one", to: Routes.signup_path(@conn, :index) %>
</p>
</div>
<% end %>
</div>
</div>
</div>

View file

@ -0,0 +1,36 @@
<div class="page__auth-simple ">
<h1>Create a new user</h1>
<%= f = form_for @form, Routes.create_user_path(@conn, :create), [id: "create-form", method: :post] %>
<!-- <div class="section__preview"> -->
<!-- <div class="preview__card"> -->
<!-- <label class="card__upload" for="upload_bg"> -->
<!-- <div class="card__bg" style="background-image: url(<%= Faker.Avatar.image_url() %>)"></div> -->
<!-- <input name="image[upload]" type="file" id="upload_bg" aria-label="Image file selector"> -->
<!-- </label> -->
<!-- <div class="card__bar"> -->
<!-- <label class="card__upload icon" for="upload_icon"> -->
<!-- <div class="bar__icon" style="background-image: url(<%= Faker.Avatar.image_url(60,60) %>)"></div> -->
<!-- <input name="icon[upload]" type="file" id="upload_icon" aria-label="Icon file selector"> -->
<!-- </label> -->
<!-- </div> -->
<!-- </div> -->
<!-- </div> -->
<div class="form__container">
<%= label f, :name, "What is your preferred name?" %>
<%= text_input f, :name, required: true %>
<%= error_tag f, :name %>
</div>
<div class="form__container">
<%= label f, :username, "What username will you use?" %>
<%= text_input f, :username, required: true %>
<%= error_tag f, :username %>
</div>
<div class="form__container">
<label for="bio">Tell us a bit about yourself</label>
<%= label f, :summary, "Tell us a bit about yourself." %>
<%= textarea f, :summary, required: true %>
<%= error_tag f, :summary %>
</div>
<button type="submit">Create</button>
</form>
</div>

View file

View file

@ -1,5 +1,15 @@
<main role="main" class="container">
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
<% info = get_flash(@conn, :info) %>
<% error = get_flash(@conn, :error) %>
<%= if info || error do %>
<div id="flash-messages">
<%= if info do %>
<p class="alert alert-info" role="alert"><%= info %></p>
<% end %>
<%= if error do %>
<p class="alert alert-danger" role="alert"><%= error %></p>
<% end %>
</div>
<% end %>
<%= @inner_content %>
</main>

View file

@ -0,0 +1,18 @@
defmodule VoxPublica.Web.Layout.HeaderLive do
use VoxPublica.Web, :live_component
import VoxPublica.Web.CommonHelper
def update(assigns, socket) do
{
:ok,
socket
|> assign(assigns)
}
end
def handle_params(%{"signout" => _name} = _data, _socket) do
IO.inspect("signout!")
end
end

View file

@ -0,0 +1,57 @@
<header class="cpub__header">
<nav
role="navigation"
aria-label="Main navigation">
<div class="header__left">
<% homepage = if @current_user, do: "/~", else: "/" %>
<%= live_redirect to: homepage do %>
<h3>VoxPublica</h3>
<% end %>
</div>
<div class="header__right">
<%= if @current_user do %>
<div class="box__info">
<%= live_redirect to: "/@"<> e(@current_user, :username, "") do %>
<img src="<%= e(@current_user, :icon_url, "") %>" />
<h3><%= e(@current_user, :name, "Me") %></h3>
<% end %>
<details class="drawer__profile ligth" >
<summary class="user__dropdown">
<span class="right__notification"><i class="feather-plus"></i></span>
</summary>
<ul class="dropdown__list">
<h2>Create</h2>
<li phx-target="#write_widget" phx-click="toggle_post"><i class="feather-edit"></i> Write a post</li>
</ul>
</details>
<span class="right__notification"><i class="feather-bell"></i></span>
<details class="drawer__profile ligth" >
<summary class="user__dropdown">
<span class="right__notification"><i class="feather-chevron-down"></i></span>
</summary>
<ul class="dropdown__list">
<li><%= live_redirect to: "/@"<> e(@current_user, :username, "me") do %>Profile<% end %></li>
<%= if @current_user.is_instance_admin do %>
<li><%= live_redirect to: "/admin/settings/access" do %>Admin<% end %></li>
<% end %>
<li><%= live_redirect to: "/settings" do %>Settings<% end %></li>
<li><a href="/logout">Logout</a></li>
</ul>
</details>
</div>
<% end %>
<%= if @current_user == nil do %>
<div class="panel__item">
<%= live_redirect to: "/login", class: "button" do %>
<i class="feather-log-in"></i> Log in
<% end %>
</div>
<div class="panel__item">
<%= live_redirect to: "/signup", class: "button" do %>
<i class="feather-zap"></i> Sign up
<% end %>
</div>
<% end %>
</div>
</nav>
</header>

View file

@ -1,13 +1,27 @@
<div id="template" class="page__container">
<main role="main" class="container">
<p class="alert alert-info" role="alert"
phx-click="lv:clear-flash"
phx-value-key="info"><%= live_flash(@flash, :info) %></p>
<p class="alert alert-danger" role="alert"
phx-click="lv:clear-flash"
phx-value-key="error"><%= live_flash(@flash, :error) %></p>
<%= live_component(
@socket,
VoxPublica.Web.Layout.HeaderLive,
id: :my_header,
current_user: @current_user
) %>
<div class="page <%= if !@current_user , do: "guest", else: "logged" %>">
<% info = live_flash(@flash, :info) %>
<% error = live_flash(@flash, :error) %>
<%= if info || error do %>
<div id="flash-messages">
<%= if info do %>
<p class="alert alert-info" role="alert"
phx-click="lv:clear-flash"
phx-value-key="info"><%= info %></p>
<% end %>
<%= if error do %>
<p class="alert alert-danger" role="alert"
phx-click="lv:clear-flash"
phx-value-key="error"><%= error %></p>
<% end %>
</div>
<% end %>
<%= @inner_content %>
</main>
</div>
</div>

View file

@ -0,0 +1,70 @@
<main class="guest__container page__auth-grid">
<aside class="auth__info">
<div
role="img"
aria-label="instance representative image"
title="instance representative image"
class="grid__background"></div>
<h2>About this instance</h2>
<p>It has been created for clear, uncluttered and elegant designs following a minimal and flat style pattern. For syntax highlighting it aims to ensure an undisturbed focus on important parts of the code, a good readability and a quick visual distinction between the different syntax elements.
</p>
<h3>Useful links</h3>
<ol>
<li><%= link "Code of Conduct", to: "/#" %></li>
<li><%= link "Accessibility guidelines", to: "/#" %></li>
<li><%= link "Terms and Conditions", to: "/#" %></li>
</ol>
</aside>
<section class="grid__form">
<h1>Log in</h1>
<%= if @error do %>
<div
role="status"
class="box__warning">
<%= case @error do %>
<% :not_found -> %>
<span>Your username and/or password are incorrect.</span>
<% :email_not_confirmed -> %>
<span>You must confirm your email address before logging in. Check your email.</span>
<% end %>
</div>
<% end %>
<div class="form__wrapper">
<%= f = form_for @form, Routes.login_path(@conn, :create), [method: :post, id: "login-form"] %>
<div class="form__container">
<%= label f, :email, "Type your email" %>
<%= text_input f,
:email,
[
aria_describedby: "required-message",
required: true
] %>
<%= error_tag f, :email %>
</div>
<div class="form__container">
<%= label f, :password, "Type your password" %>
<%= password_input f,
:password,
[
aria_describedby: "required-message",
required: true
] %>
<%= error_tag f, :password %>
</div>
<%= submit "Log in" %>
</form>
<aside class="auth__helpers">
<p>
<span>&#x01f44b;</span>
Don&#39;t have an account yet?
<%= link "Create a new one.", to: Routes.signup_path(@conn, :index) %>
</p>
<p>
<span>&#x01f9d0;</span>
Trouble logging in?
<%= link "Reset your password.", to: Routes.forgot_password_path(@conn, :index) %>.
</p>
</aside>
</div>
</section>
</main>

View file

@ -0,0 +1,71 @@
<main class="guest__container page__auth-grid">
<aside class="auth__info">
<div
role="img"
aria-label="instance representative image"
title="instance representative image"
class="grid__background"></div>
<h2>About this instance</h2>
<p>It has been created for clear, uncluttered and elegant designs following a minimal and flat style pattern. For syntax highlighting it aims to ensure an undisturbed focus on important parts of the code, a good readability and a quick visual distinction between the different syntax elements.
</p>
<h3>Useful links</h3>
<ol>
<li><%= link "Code of Conduct", to: "/#" %></li>
<li><%= link "Accessibility guidelines", to: "/#" %></li>
<li><%= link "Terms and Conditions", to: "/#" %></li>
</ol>
</aside>
<section class="grid__form">
<%= if @registered do %>
<div
role="status"
class="form__confirmation">
<h2>Hooray! You are registered</h2>
<p>
Now we need you to confirm your email address. We've mailed
you a link. Please click it to continue.
</p>
</div>
<% else %>
<h1>Create a new account</h1>
<%= if @error == :taken do %>
<div role="status" class="box__warning">
<span>This email is taken. Did you mean to <%= link "log in", to: "/login" %>?
</div>
<% end %>
<div class="form__wrapper">
<%= f = form_for @form, "/signup", [id: "signup-form", phx_submit: :submit] %>
<div class="form__container">
<%= label f, :email, "Type your email" %>
<%= text_input f,
:email,
[
aria_describedby: "required-message",
required: true
] %>
<%= error_tag f, :email %>
</div>
<div class="form__container">
<%= label f, :password, "Type your password (11 chars min)" %>
<%= password_input f,
:password,
[
aria_describedby: "required-message",
required: true
]
%>
<%= error_tag f, :password %>
</div>
<%= submit "Sign up" %>
</form>
<aside class="auth__helpers">
<p>
<span>&#x01f44b; </span>
Do you already have an account?
<%= link "Log in", to: "/login" %>.
</p>
</aside>
</div>
<% end %>
</section>
</main>

View file

@ -0,0 +1,10 @@
<h1>User Switcher</h1>
<ul class="user-list">
<%= for user <- @users do %>
<li>
<%= link "@" <> user.character.username,
to: "/switch-user/@" <> user.character.username %>
</li>
<% end %>
</ul>

View file

@ -0,0 +1,3 @@
defmodule VoxPublica.Web.ChangePasswordView do
use VoxPublica.Web, :view
end

View file

@ -0,0 +1,3 @@
defmodule VoxPublica.Web.ConfirmEmailView do
use VoxPublica.Web, :view
end

View file

@ -0,0 +1,3 @@
defmodule VoxPublica.Web.CreateUserView do
use VoxPublica.Web, :view
end

View file

@ -0,0 +1,3 @@
defmodule VoxPublica.Web.EmailView do
use VoxPublica.Web, :view
end

View file

@ -0,0 +1,3 @@
defmodule VoxPublica.Web.LoginView do
use VoxPublica.Web, :view
end

View file

@ -0,0 +1,3 @@
defmodule VoxPublica.Web.ResetPasswordView do
use VoxPublica.Web, :view
end

View file

@ -0,0 +1,3 @@
defmodule VoxPublica.Web.SignupView do
use VoxPublica.Web, :view
end

View file

@ -0,0 +1,3 @@
defmodule VoxPublica.Web.SwitchUserView do
use VoxPublica.Web, :view
end

View file

@ -8,6 +8,7 @@ defmodule VoxPublica.Web do
import Plug.Conn
import VoxPublica.Web.Gettext
alias VoxPublica.Web.Router.Helpers, as: Routes
alias VoxPublica.Web.Plugs.{MustBeGuest, MustLogIn}
end
end

38
mess.exs Normal file
View file

@ -0,0 +1,38 @@
# Copyright (c) 2020 James Laver, mess Contributors
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
if not Code.ensure_loaded?(Mess) do
defmodule Mess do
@sources [path: "deps.path", git: "deps.git", hex: "deps.hex"]
@newline ~r/(?:\r\n|[\r\n])/
@parser ~r/^(?<indent>\s*)((?<package>[a-z_][a-z0-9_]+)\s*=\s*"(?<value>[^"]+)")?(?<post>.*)/
@git_branch ~r/(?<repo>[^#]+)(#(?<branch>.+))?/
def deps(sources \\ @sources, deps), do: deps(Enum.flat_map(sources, fn {k,v} -> read(v, k) end), deps, :deps)
defp deps(packages, deps, :deps), do: deps(Enum.flat_map(packages, &dep_spec/1), deps, :uniq)
defp deps(packages, deps, :uniq), do: Enum.uniq_by(deps ++ packages, &elem(&1, 0))
defp read(path, kind) when is_binary(path), do: read(File.read(path), kind)
defp read({:error, :enoent}, _kind), do: []
defp read({:ok, file}, kind), do: Enum.map(String.split(file, @newline), &read_line(&1, kind))
defp read_line(line, kind), do: Map.put(Regex.named_captures(@parser, line), :kind, kind)
defp dep_spec(%{"package" => ""}), do: []
defp dep_spec(%{"package" => p, "value" => v, :kind => :hex}), do: pkg(p, v, override: true)
defp dep_spec(%{"package" => p, "value" => v, :kind => :path}), do: pkg(p, path: v, override: true)
defp dep_spec(%{"package" => p, "value" => v, :kind => :git}), do: git(v, p)
defp git(line, p) when is_binary(line), do: git(Regex.named_captures(@git_branch, line), p)
defp git(%{"branch" => "", "repo" => r}, p), do: pkg(p, git: r, override: true)
defp git(%{"branch" => b, "repo" => r}, p), do: pkg(p, git: r, branch: b, override: true)
defp pkg(name, opts), do: [{String.to_atom(name), opts}]
defp pkg(name, version, opts), do: [{String.to_atom(name), version, opts}]
end
end

94
mix.exs
View file

@ -1,4 +1,6 @@
Code.eval_file("mess.exs")
defmodule VoxPublica.MixProject do
use Mix.Project
def project do
@ -10,103 +12,24 @@ defmodule VoxPublica.MixProject do
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps()
deps: Mess.deps [
{:phoenix_live_reload, "~> 1.2", only: :dev},
{:dbg, "~> 1.0", only: [:dev, :test]},
{:floki, ">= 0.0.0", only: [:dev, :test]},
]
]
end
def application do
[
mod: {VoxPublica.Application, []},
extra_applications: [:logger, :runtime_tools, :os_mon]
extra_applications: [:logger, :runtime_tools, :ssl, :bamboo, :bamboo_smtp]
]
end
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
defp deps do
[
{:phoenix_live_view, "~> 0.14"},
{:phoenix_html, "~> 2.11"},
{:phoenix_live_dashboard, "~> 0.2.0"},
{:plug_cowboy, "~> 2.0"},
{:phoenix, "~> 1.5.3"},
{:phoenix_ecto, "~> 4.1"},
{:ecto_sql, "~> 3.4"},
{:postgrex, ">= 0.0.0"},
{:telemetry_metrics, "~> 0.4"},
{:telemetry_poller, "~> 0.4"},
{:gettext, "~> 0.11"},
{:jason, "~> 1.0"},
{:pointers_ulid, "~> 0.2"},
# {:pointers_ulid, path: "../pointers_ulid", override: true},
# {:pointers, "~> 0.5.1"},
# {:pointers, git: "https://github.com/commonspub/pointers", branch: "main"},
{:pointers, "0.5.1", override: true},
{:flexto, "~> 0.2.1", override: true},
# {:flexto, path: "../flexto", override: true},
{:cpub_accounts, "~> 0.1"},
# {:cpub_accounts, git: "https://github.com/commonspub/cpub_accounts", branch: "main"},
# {:cpub_accounts, path: "../cpub_accounts", override: true},
{:cpub_blocks, "~> 0.1"},
# {:cpub_blocks, git: "https://github.com/commonspub/cpub_blocks", branch: "main"},
# {:cpub_blocks, path: "../cpub_blocks", override: true},
# {:cpub_bookmarks, git: "https://github.com/commonspub/cpub_bookmarks", branch: "main"},
# # {:cpub_bookmarks, path: "../cpub_bookmarks", override: true},
{:cpub_characters, "~> 0.1"},
# {:cpub_characters, git: "https://github.com/commonspub/cpub_characters", branch: "main"},
# {:cpub_characters, path: "../cpub_characters", override: true},
# {:cpub_circles, "~> 0.1"},
# {:cpub_circles, git: "https://github.com/commonspub/cpub_circles", branch: "main"},
# {:cpub_circles, path: "../cpub_circles", override: true},
# {:cpub_comments, "~> 0.1"},
# {:cpub_comments, git: "https://github.com/commonspub/cpub_comments", branch: "main"},
# {:cpub_comments, path: "../cpub_comments", override: true},
# {:cpub_communities, "~> 0.1"},
# {:cpub_communities, git: "https://github.com/commonspub/cpub_communities", branch: "main"},
# {:cpub_communities, path: "../cpub_communities", override: true},
{:cpub_emails, "~> 0.1"},
# {:cpub_emails, git: "https://github.com/commonspub/cpub_emails", branch: "main"},
# {:cpub_emails, path: "../cpub_emails", override: true},
{:cpub_local_auth, "~> 0.1"},
# {:cpub_local_auth, git: "https://github.com/commonspub/cpub_local_auth", branch: "main"},
# {:cpub_local_auth, path: "../cpub_local_auth", override: true},
{:cpub_profiles, "~> 0.1"},
# {:cpub_profiles, git: "https://github.com/commonspub/cpub_profiles", branch: "main"},
# {:cpub_profiles, path: "../cpub_profiles", override: true},
{:cpub_users, "~> 0.1"},
# {:cpub_users, git: "https://github.com/commonspub/cpub_users", branch: "main"},
# {:cpub_users, path: "../cpub_users", override: true},
{:cpub_actors, git: "https://github.com/commonspub/cpub_actors", branch: "main"},
{:activity_pub, git: "https://gitlab.com/CommonsPub/activitypub.git", branch: :develop},
{:oban, "~> 2.0.0"},
# {:fast_sanitize, "~> 0.2.2"}, # html sanitisation
{:faker, "~> 0.14"}, # fake data generation
{:phoenix_live_reload, "~> 1.2", only: :dev},
{:dbg, "~> 1.0", only: [:dev, :test]},
{:floki, ">= 0.0.0", only: :test},
]
end
defp aliases do
[
"js.deps.get": ["cmd npm install --prefix assets"],
@ -117,4 +40,5 @@ defmodule VoxPublica.MixProject do
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"]
]
end
end

View file

@ -1,6 +1,8 @@
%{
"activity_pub": {:git, "https://gitlab.com/CommonsPub/activitypub.git", "c5bea6c81f7ec1725a6acb724c65621718c97767", [branch: :develop]},
"activity_pub": {:git, "https://gitlab.com/CommonsPub/activitypub.git", "c5bea6c81f7ec1725a6acb724c65621718c97767", [branch: "develop"]},
"argon2_elixir": {:hex, :argon2_elixir, "2.3.0", "e251bdafd69308e8c1263e111600e6d68bd44f23d2cccbe43fcb1a417a76bc8e", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "28ccb63bff213aecec1f7f3dde9648418b031f822499973281d8f494b9d5a3b3"},
"bamboo": {:hex, :bamboo, "1.5.0", "1926107d58adba6620450f254dfe8a3686637a291851fba125686fa8574842af", [:mix], [{:hackney, ">= 1.13.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d5f3d04d154e80176fd685e2531e73870d8700679f14d25a567e448abce6298d"},
"bamboo_smtp": {:hex, :bamboo_smtp, "3.0.0", "b7f0c371af96a1cb7131908918b02abb228f9db234910bf10cf4fb177c083259", [:mix], [{:bamboo, "~> 1.2", [hex: :bamboo, repo: "hexpm", optional: false]}, {:gen_smtp, "~> 0.15.0", [hex: :gen_smtp, repo: "hexpm", optional: false]}], "hexpm", "77cb1fa3076b24109e54df622161fe1e5619376b4ecf86d8b99b46f327acc49f"},
"cachex": {:hex, :cachex, "3.3.0", "6f2ebb8f27491fe39121bd207c78badc499214d76c695658b19d6079beeca5c2", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "d90e5ee1dde14cef33f6b187af4335b88748b72b30c038969176cd4e6ccc31a1"},
"certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
@ -8,17 +10,17 @@
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
"cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"},
"cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"},
"cpub_accounts": {:hex, :cpub_accounts, "0.1.0", "a572d258fe0c16a67248ee26225e3b567ae542ec371ef3933ffda6912b89d0b9", [:mix], [{:pointers, "~> 0.5.1", [hex: :pointers, repo: "hexpm", optional: false]}], "hexpm", "351bdbd2b0991a6b121218d7a876d13fa9402c4be419d4a27c52bc09280d1ce1"},
"cpub_actors": {:git, "https://github.com/commonspub/cpub_actors", "1ccfa5ce29600af20aee9b8ea58b173c4d6208ee", [branch: "main"]},
"cpub_accounts": {:git, "https://github.com/commonspub/cpub_accounts", "b731e9e06d9cbf60a10e866ada870c1770a2b334", [branch: "main"]},
"cpub_actors": {:git, "https://github.com/commonspub/cpub_actors", "b3c591fa3227d70c7a159fb14d18d3295a514346", [branch: "main"]},
"cpub_blocks": {:hex, :cpub_blocks, "0.1.0", "1b9032e4f14ab36a6960fd9a7d4d9275bf146714123f5b72b8b62b454ef0f187", [:mix], [{:pointers, "~> 0.5.1", [hex: :pointers, repo: "hexpm", optional: false]}], "hexpm", "6244ccccd572482a9389613927c98ac2aa14ea13340d61c8e0731489d37fcc73"},
"cpub_characters": {:hex, :cpub_characters, "0.1.0", "e4f13e8f59e499faa3341f37696655ea9eea4f5d0856a34f457e159c5363b6b5", [:mix], [{:pointers, "~> 0.5.1", [hex: :pointers, repo: "hexpm", optional: false]}], "hexpm", "d9b4a29dfee4d520363e4a9cc7ab07253c5319ef8e362564338704cdb6c26178"},
"cpub_emails": {:hex, :cpub_emails, "0.1.0", "157cae393aa801471106c0dd35e8beb5f3123ac8aa4b846d56cb1d88ce41c0a5", [:mix], [{:pointers, "~> 0.5.1", [hex: :pointers, repo: "hexpm", optional: false]}], "hexpm", "a1cfffb3ba9fcf88dd30bb27baa0ff1a56715d631d18f45d06f7ebc3ea4cf83b"},
"cpub_emails": {:git, "https://github.com/commonspub/cpub_emails", "9ce5af3da39c8d8488d3de5fbb760e4c7706578c", [branch: "main"]},
"cpub_local_auth": {:hex, :cpub_local_auth, "0.1.0", "f5d479af1c16a99f6f7e6802fae56f56d1ec590fd502fabff649dbaecd0d5f2d", [:mix], [{:argon2_elixir, "~> 2.3.0", [hex: :argon2_elixir, repo: "hexpm", optional: false]}, {:pointers, "~> 0.5.1", [hex: :pointers, repo: "hexpm", optional: false]}], "hexpm", "f3a08714f2b285bb32a2f7775cd8458c02d0d7079c97028c931aa08827aaf996"},
"cpub_profiles": {:hex, :cpub_profiles, "0.1.0", "e5b627ee16691bb04865ae3eb97cabfdd9f28d4b9fb220438fde0ccbf05f936c", [:mix], [{:pointers, "~> 0.5.1", [hex: :pointers, repo: "hexpm", optional: false]}], "hexpm", "cf54ca9e0adecdbdf3d79f3bc90d4c3c97053f106359a5c09d857d389d27b69c"},
"cpub_users": {:hex, :cpub_users, "0.1.0", "ac95256aa4eb8032c2239fd4eb5351e28c691b477707bbf1230a86c7048f9a41", [:mix], [{:pointers, "~> 0.5.1", [hex: :pointers, repo: "hexpm", optional: false]}], "hexpm", "f31af6fbd0d802fb1670fe950b968a90589ffaaaed85fd850e72acb5ba0a9346"},
"db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"},
"dbg": {:hex, :dbg, "1.0.1", "9c29813e5df8b4d275325416523d511e315656b8ac27a60519791f9cf476d83d", [:mix], [], "hexpm", "866159f496a1ad9b959501f16db3d1338bb6cef029a75a67ca5615d25b38345f"},
"decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"},
"decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"},
"ecto": {:hex, :ecto, "3.4.6", "08f7afad3257d6eb8613309af31037e16c36808dfda5a3cd0cb4e9738db030e4", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6f13a9e2a62e75c2dcfc7207bfc65645ab387af8360db4c89fee8b5a4bf3f70b"},
"ecto_sql": {:hex, :ecto_sql, "3.4.5", "30161f81b167d561a9a2df4329c10ae05ff36eca7ccc84628f2c8b9fa1e43323", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "31990c6a3579b36a3c0841d34a94c275e727de8b84f58509da5f1b2032c98ac2"},
"elixir_make": {:hex, :elixir_make, "0.6.1", "8faa29a5597faba999aeeb72bbb9c91694ef8068f0131192fb199f98d32994ef", [:mix], [], "hexpm", "35d33270680f8d839a4003c3e9f43afb595310a592405a00afc12de4c7f55a18"},
@ -29,18 +31,20 @@
"file_system": {:hex, :file_system, "0.2.8", "f632bd287927a1eed2b718f22af727c5aeaccc9a98d8c2bd7bff709e851dc986", [:mix], [], "hexpm", "97a3b6f8d63ef53bd0113070102db2ce05352ecf0d25390eb8d747c2bde98bca"},
"flexto": {:hex, :flexto, "0.2.1", "95a27fbc2c7b3a171d91b52c0211b5ee8572ea6fdb40af474b79e22f87a6bc76", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "4196c1e56a9759224e7d074ae33cdf5f2689d20f902e052eff4e0fd4b814a677"},
"floki": {:hex, :floki, "0.28.0", "0d0795a17189510ee01323e6990f906309e9fc6e8570219135211f1264d78c7f", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "db1549560874ebba5a6367e46c3aec5fedd41f2757ad6efe567efb04b4d4ee55"},
"gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"},
"gettext": {:hex, :gettext, "0.18.1", "89e8499b051c7671fa60782faf24409b5d2306aa71feb43d79648a8bc63d0522", [:mix], [], "hexpm", "e70750c10a5f88cb8dc026fc28fa101529835026dec4a06dba3b614f2a99c7a9"},
"hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"},
"html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"},
"http_signatures": {:git, "https://git.pleroma.social/pleroma/http_signatures.git", "293d77bb6f4a67ac8bde1428735c3b42f22cbb30", [ref: "293d77bb6f4a67ac8bde1428735c3b42f22cbb30"]},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"},
"jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"},
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
"jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm", "318c59078ac220e966d27af3646026db9b5a5e6703cb2aa3e26bcfaba65b7433"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "1.4.0", "5066f14944b470286146047d2f73518cf5cca82f8e4815cf35d196b58cf07c47", [:mix], [], "hexpm", "75fa42c4228ea9a23f70f123c74ba7cece6a03b1fd474fe13f6a7a85c6ea4ff6"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"nimble_pool": {:hex, :nimble_pool, "0.1.0", "ffa9d5be27eee2b00b0c634eb649aa27f97b39186fec3c493716c2a33e784ec6", [:mix], [], "hexpm", "343a1eaa620ddcf3430a83f39f2af499fe2370390d4f785cd475b4df5acaf3f9"},
"oban": {:hex, :oban, "2.0.0", "e6ce70d94dd46815ec0882a1ffb7356df9a9d5b8a40a64ce5c2536617a447379", [:mix], [{:ecto_sql, ">= 3.4.3", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cf574813bd048b98a698aa587c21367d2e06842d4e1b1993dcd6a696e9e633bd"},
"ok": {:hex, :ok, "2.3.0", "0a3d513ec9038504dc5359d44e14fc14ef59179e625563a1a144199cdc3a6d30", [:mix], [], "hexpm", "f0347b3f8f115bf347c704184b33cf084f2943771273f2b98a3707a5fa43c4d5"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
"phoenix": {:hex, :phoenix, "1.5.4", "0fca9ce7e960f9498d6315e41fcd0c80bfa6fbeb5fa3255b830c67fdfb7e703f", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4e516d131fde87b568abd62e1b14aa07ba7d5edfd230bab4e25cc9dedbb39135"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.2.0", "4ac3300a22240a37ed54dfe6c0be1b5623304385d1a2c210a70f011d9e7af7ac", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "59e7e2a550d7ea082a665c0fc29485f06f55d1a51dd02f513aafdb9d16fc72c4"},
@ -52,10 +56,11 @@
"plug": {:hex, :plug, "1.10.4", "41eba7d1a2d671faaf531fa867645bd5a3dce0957d8e2a3f398ccff7d2ef017f", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ad1e233fe73d2eec56616568d260777b67f53148a999dc2d048f4eb9778fe4a0"},
"plug_cowboy": {:hex, :plug_cowboy, "2.3.0", "149a50e05cb73c12aad6506a371cd75750c0b19a32f81866e1a323dda9e0e99d", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bc595a1870cef13f9c1e03df56d96804db7f702175e4ccacdb8fc75c02a7b97e"},
"plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"},
"pointers": {:hex, :pointers, "0.5.1", "403946f7e041d5624cbce6dbf308dfc65d336a97019c398b369cae062626b22d", [:mix], [{:ecto_sql, "~> 3.4", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:flexto, "~> 0.1", [hex: :flexto, repo: "hexpm", optional: false]}, {:pointers_ulid, "~> 0.2", [hex: :pointers_ulid, repo: "hexpm", optional: false]}], "hexpm", "0ee81db5fa11008a508b6b029b59a2be66a1264879b5361d4586c1533f2e818a"},
"pointers": {:git, "https://github.com/commonspub/pointers", "c5f3d343ba113883540397a2b1972b81f1e043a3", [branch: "main"]},
"pointers_ulid": {:hex, :pointers_ulid, "0.2.2", "305df7d45d5227467bb9b9441f7f06fe5386390f8a4daf8084f28a58ea3e14f7", [:mix], [{:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.4", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "5cca67c892a9af22030930762340d7ce82a0cc91d75559ca085d855cfed3de5b"},
"postgrex": {:hex, :postgrex, "0.15.5", "aec40306a622d459b01bff890fa42f1430dac61593b122754144ad9033a2152f", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ed90c81e1525f65a2ba2279dbcebf030d6d13328daa2f8088b9661eb9143af7f"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
"recase": {:hex, :recase, "0.6.0", "1dd2dd2f4e06603b74977630e739f08b7fedbb9420cc14de353666c2fc8b99f4", [:mix], [], "hexpm", "8712e318420a228eb2e6366ada230148ed3a4316a798319edd5512f64d78c990"},
"sleeplocks": {:hex, :sleeplocks, "1.1.1", "3d462a0639a6ef36cc75d6038b7393ae537ab394641beb59830a1b8271faeed3", [:rebar3], [], "hexpm", "84ee37aeff4d0d92b290fff986d6a95ac5eedf9b383fadfd1d88e9b84a1c02e1"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"},
"telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},

View file

@ -7,17 +7,17 @@ defmodule VoxPublica.AccountsTest do
test "works" do
attrs = Fake.account()
assert {:ok, account} = Accounts.register(attrs)
assert {:ok, account} = Accounts.signup(attrs)
assert account.login_credential.identity == attrs[:email]
assert Argon2.verify_pass(attrs[:password], account.login_credential.password_hash)
end
test "emails must be unique" do
attrs = Fake.account()
assert {:ok, account} = Accounts.register(attrs)
assert {:ok, account} = Accounts.signup(attrs)
assert account.login_credential.identity == attrs[:email]
assert Argon2.verify_pass(attrs[:password], account.login_credential.password_hash)
assert {:error, :taken} = Accounts.register(attrs)
assert {:error, :taken} = Accounts.signup(attrs)
end
end
@ -26,10 +26,10 @@ defmodule VoxPublica.AccountsTest do
test "works given an account" do
attrs = Fake.account()
assert {:ok, account} = Accounts.register(attrs)
assert {:ok, account} = Accounts.signup(attrs)
assert {:ok, account} = Accounts.confirm_email(account)
assert account.email.email_confirmed_at
assert is_nil(account.email.email_confirm_token)
assert account.email.confirmed_at
assert is_nil(account.email.confirm_token)
end
end
@ -38,13 +38,13 @@ defmodule VoxPublica.AccountsTest do
test "account must have a confirmed email" do
attrs = Fake.account()
assert {:ok, account} = Accounts.register(attrs)
assert {:ok, account} = Accounts.signup(attrs)
assert {:error, :email_not_confirmed} == Accounts.login(attrs)
end
test "success" do
attrs = Fake.account()
assert {:ok, account} = Accounts.register(attrs)
assert {:ok, account} = Accounts.signup(attrs)
{:ok, _} = Accounts.confirm_email(account)
assert {:ok, account} = Accounts.login(attrs)
assert account.email.email == attrs[:email]

View file

@ -7,9 +7,9 @@ defmodule VoxPublica.ActivityPub.AdapterTest do
describe "actor fetching" do
test "by username" do
assert {:ok, account} = Accounts.register(Fake.account())
assert {:ok, account} = Accounts.signup(Fake.account())
attrs = Fake.user()
assert {:ok, user} = Users.create(account, attrs)
assert {:ok, user} = Users.create(attrs, account)
assert {:ok, actor} = Adapter.get_actor_by_username(attrs.username)
assert actor.data["summary"] == attrs.summary
assert actor.data["preferredUsername"] == attrs.username

View file

@ -6,9 +6,9 @@ defmodule VoxPublica.ActivityPub.IntegrationTest do
alias VoxPublica.Fake
test "fetch users from AP API" do
assert {:ok, account} = Accounts.register(Fake.account())
assert {:ok, account} = Accounts.signup(Fake.account())
attrs = Fake.user()
assert {:ok, user} = Users.create(account, attrs)
assert {:ok, user} = Users.create(attrs, account)
conn =
build_conn()

View file

@ -24,7 +24,8 @@ defmodule VoxPublica.ConnCase do
import Phoenix.ConnTest
import Phoenix.LiveViewTest
import VoxPublica.ConnCase
import VoxPublica.ConnHelpers
import VoxPublica.Test.ConnHelpers
import VoxPublica.Test.FakeHelpers
alias VoxPublica.Fake
alias VoxPublica.Web.Router.Helpers, as: Routes
@ -40,7 +41,7 @@ defmodule VoxPublica.ConnCase do
Ecto.Adapters.SQL.Sandbox.mode(VoxPublica.Repo, {:shared, self()})
end
{:ok, conn: Phoenix.ConnTest.build_conn()}
{:ok, []}
end
end

View file

@ -1,14 +1,94 @@
defmodule VoxPublica.ConnHelpers do
defmodule VoxPublica.Test.ConnHelpers do
import ExUnit.Assertions
import Plug.Conn
import Phoenix.ConnTest
import Phoenix.LiveViewTest
# alias VoxPublica.Accounts
alias CommonsPub.Accounts.Account
alias CommonsPub.Users.User
@endpoint VoxPublica.Web.Endpoint
### conn
def session_conn(conn \\ build_conn()), do: Plug.Test.init_test_session(conn, %{})
def conn(), do: conn(session_conn(), [])
def conn(%Plug.Conn{}=conn), do: conn(conn, [])
def conn(filters) when is_list(filters), do: conn(session_conn(), filters)
def conn(conn, filters) when is_list(filters),
do: Enum.reduce(filters, conn, &conn(&2, &1))
def conn(conn, {:account, %Account{id: id}}),
do: put_session(conn, :account_id, id)
def conn(conn, {:account, account_id}) when is_binary(account_id),
do: put_session(conn, :account_id, account_id)
def conn(conn, {:user, %User{id: id}}),
do: put_session(conn, :user_id, id)
def conn(conn, {:user, user_id}) when is_binary(user_id),
do: put_session(conn, :user_id, user_id)
def find_flash(doc) do
messages = Floki.find(doc, "#flash-messages p")
case messages do
[_, _ | _] -> throw :too_many_flashes
short -> short
end
end
def assert_flash(p, kind, message) do
assert_flash_kind(p, kind)
assert_flash_message(p, message)
end
def assert_flash_kind(flash, :error) do
classes = floki_attr(flash, :class)
assert "alert" in classes
assert "alert-danger" in classes
end
def assert_flash_kind(flash, :info) do
classes = floki_attr(flash, :class)
assert "alert" in classes
assert "alert-info" in classes
end
def assert_flash_message(flash, %Regex{}=r),
do: assert(Floki.text(flash) =~ r)
def assert_flash_message(flash, bin) when is_binary(bin),
do: assert(Floki.text(flash) == bin)
def find_form_error(doc, name),
do: Floki.find(doc, "span.invalid-feedback[phx-feedback-for='#{name}']")
def assert_field_good(doc, name) do
assert [field] = Floki.find(doc, "#" <> name)
assert [] == find_form_error(doc, name)
field
end
def assert_field_error(doc, name, error) do
assert [field] = Floki.find(doc, "#" <> name)
assert [err] = find_form_error(doc, name)
assert Floki.text(err) =~ error
field
end
### floki_attr
def floki_attr(elem, :class),
do: Enum.flat_map(floki_attr(elem, "class"), &String.split(&1, ~r/\s+/, trim: true))
def floki_attr(elem, attr) when is_binary(attr),
do: Floki.attribute(elem, attr)
def floki_response(conn, code \\ 200) do
assert {:ok, html} = Floki.parse_document(html_response(conn, 200))
assert {:ok, html} = Floki.parse_document(html_response(conn, code))
html
end
@ -24,7 +104,7 @@ defmodule VoxPublica.ConnHelpers do
{view, doc}
end
def floki_click(view, event, value \\ %{}) do
def floki_click(view, value \\ %{}) do
assert {:ok, doc} = Floki.parse_fragment(render_click(view, value))
doc
end

View file

@ -0,0 +1,6 @@
defmodule VoxPublica.DataHelpers do
# import ExUnit.Assertions
# alias VoxPublica.Fake
end

View file

@ -0,0 +1,18 @@
defmodule VoxPublica.Test.FakeHelpers do
alias CommonsPub.Accounts.Account
alias VoxPublica.{Accounts, Fake, Repo, Users}
import ExUnit.Assertions
def fake_account!(attrs \\ %{}) do
cs = Accounts.signup_changeset(Fake.account(attrs))
assert {:ok, account} = Repo.insert(cs)
account
end
def fake_user!(%Account{}=account, attrs \\ %{}) do
assert {:ok, user} = Users.create(Fake.user(attrs), account)
user
end
end

View file

@ -4,28 +4,28 @@ defmodule VoxPublica.UsersTest do
alias VoxPublica.{Accounts, Fake, Users}
test "creation works" do
assert {:ok, account} = Accounts.register(Fake.account())
assert {:ok, account} = Accounts.signup(Fake.account())
attrs = Fake.user()
assert {:ok, user} = Users.create(account, attrs)
assert {:ok, user} = Users.create(attrs, account)
assert attrs.name == user.profile.name
assert attrs.summary == user.profile.summary
assert attrs.username == user.character.username
end
test "usernames must be unique" do
assert {:ok, account} = Accounts.register(Fake.account)
assert {:ok, account} = Accounts.signup(Fake.account)
attrs = Fake.user()
assert {:ok, user} = Users.create(account, attrs)
assert {:error, changeset} = Users.create(account, attrs)
assert {:ok, user} = Users.create(attrs, account)
assert {:error, changeset} = Users.create(attrs, account)
assert %{character: character, profile: profile} = changeset.changes
assert profile.valid?
assert([username: {_,_}] = character.errors)
end
test "fetching by username" do
assert {:ok, account} = Accounts.register(Fake.account)
assert {:ok, account} = Accounts.signup(Fake.account)
attrs = Fake.user()
assert {:ok, user} = Users.create(account, attrs)
assert {:ok, user} = Users.create(attrs, account)
assert {:ok, user} = Users.by_username(attrs.username)
assert user.profile.name == attrs.name
assert user.profile.summary == attrs.summary

View file

@ -0,0 +1,100 @@
defmodule VoxPublica.Web.ChangePasswordController.Test do
use VoxPublica.ConnCase
# alias VoxPublica.Accounts
# test "form renders" do
# conn = conn()
# conn = get(conn, "/login")
# doc = floki_response(conn)
# assert [form] = Floki.find(doc, "#login-form")
# assert [_] = Floki.find(form, "#login-form_email")
# assert [_] = Floki.find(form, "#login-form_password")
# assert [_] = Floki.find(form, "button[type='submit']")
# end
# describe "required fields" do
# test "missing both" do
# conn = conn()
# conn = post(conn, "/login", %{"login_form" => %{}})
# doc = floki_response(conn)
# assert [form] = Floki.find(doc, "#login-form")
# assert [_] = Floki.find(form, "#login-form_email")
# assert [email_error] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='login-form_email']")
# assert "can't be blank" == Floki.text(email_error)
# assert [_] = Floki.find(form, "#login-form_password")
# assert [password_error] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='login-form_password']")
# assert "can't be blank" == Floki.text(password_error)
# assert [_] = Floki.find(form, "button[type='submit']")
# end
# test "missing password" do
# conn = conn()
# email = Fake.email()
# conn = post(conn, "/login", %{"login_form" => %{"email" => email}})
# doc = floki_response(conn)
# assert [form] = Floki.find(doc, "#login-form")
# assert [_] = Floki.find(form, "#login-form_email")
# assert [] == Floki.find(form, "span.invalid-feedback[phx-feedback-for='login-form_email']")
# assert [_] = Floki.find(form, "#login-form_password")
# assert [password_error] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='login-form_password']")
# assert "can't be blank" == Floki.text(password_error)
# assert [_] = Floki.find(form, "button[type='submit']")
# end
# test "missing email" do
# conn = conn()
# password = Fake.password()
# conn = post(conn, "/login", %{"login_form" => %{"password" => password}})
# doc = floki_response(conn)
# assert [form] = Floki.find(doc, "#login-form")
# assert [_] = Floki.find(form, "#login-form_email")
# assert [email_error] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='login-form_email']")
# assert [_] = Floki.find(form, "#login-form_password")
# assert [] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='login-form_password']")
# assert "can't be blank" == Floki.text(email_error)
# assert [_] = Floki.find(form, "button[type='submit']")
# end
# end
# test "not found" do
# conn = conn()
# email = Fake.email()
# password = Fake.password()
# params = %{"login_form" => %{"email" => email, "password" => password}}
# conn = post(conn, "/login", params)
# doc = floki_response(conn)
# assert [div] = Floki.find(doc, "div.box__warning")
# assert [span] = Floki.find(div, "span")
# assert Floki.text(span) =~ ~r/incorrect/
# assert [_] = Floki.find(doc, "#login-form")
# end
# test "not activated" do
# conn = conn()
# account = fake_account!()
# params = %{"login_form" =>
# %{"email" => account.email.email,
# "password" => account.login_credential.password}}
# conn = post(conn, "/login", params)
# doc = floki_response(conn)
# assert [div] = Floki.find(doc, "div.box__warning")
# assert [span] = Floki.find(div, "span")
# assert Floki.text(span) =~ ~r/confirm/
# assert [_] = Floki.find(doc, "#login-form")
# end
# test "success" do
# conn = conn()
# account = fake_account!()
# {:ok, account} = Accounts.confirm_email(account)
# params = %{"login_form" =>
# %{"email" => account.email.email,
# "password" => account.login_credential.password}}
# conn = post(conn, "/login", params)
# assert redirected_to(conn) == "/home"
# end
end

View file

@ -0,0 +1,116 @@
defmodule VoxPublica.Web.ConfirmEmailController.Test do
use VoxPublica.ConnCase
alias VoxPublica.Fake
describe "request" do
test "must be a guest" do
end
test "form renders" do
conn = conn()
conn = get(conn, "/confirm-email")
doc = floki_response(conn)
assert [form] = Floki.find(doc, "#confirm-email-form")
assert [_] = Floki.find(form, "#confirm-email-form_email")
assert [_] = Floki.find(form, "button[type='submit']")
assert [] = Floki.find(doc, ".error")
end
test "absence validation" do
conn = conn()
conn = post(conn, "/confirm-email", %{})
doc = floki_response(conn)
assert [form] = Floki.find(doc, "#confirm-email-form")
assert [_] = Floki.find(form, "#confirm-email-form_email")
assert [_] = Floki.find(form, "button[type='submit']")
assert [] = Floki.find(doc, ".error")
assert [err] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='confirm-email-form_email']")
assert "can't be blank" == Floki.text(err)
end
test "format validation" do
conn = conn()
conn = post(conn, "/confirm-email", %{"confirm_email_form" => %{"email" => Faker.Pokemon.name()}})
doc = floki_response(conn)
assert [form] = Floki.find(doc, "#confirm-email-form")
assert [_] = Floki.find(form, "#confirm-email-form_email")
assert [_] = Floki.find(form, "button[type='submit']")
assert [] = Floki.find(doc, ".error")
assert [err] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='confirm-email-form_email']")
assert "has invalid format" == Floki.text(err)
end
test "not found" do
conn = conn()
conn = post(conn, "/confirm-email", %{"confirm_email_form" => %{"email" => Fake.email()}})
doc = floki_response(conn)
assert [form] = Floki.find(doc, "#confirm-email-form")
assert [_] = Floki.find(form, "#confirm-email-form_email")
assert [_] = Floki.find(form, "button[type='submit']")
assert [err] = Floki.find(doc, ".error")
assert Floki.text(err) =~ ~r/invalid confirmation link/i
end
# TODO
# test "expired" do
# conn = conn()
# end
test "success" do
conn = conn()
account = fake_account!()
conn = get(conn, "/confirm-email")
doc = floki_response(conn)
assert [form] = Floki.find(doc, "#confirm-email-form")
assert [_] = Floki.find(form, "#confirm-email-form_email")
assert [_] = Floki.find(form, "button[type='submit']")
conn = post(recycle(conn), "/confirm-email", %{"confirm_email_form" => %{"email" => account.email.email}})
doc = floki_response(conn)
assert [] = Floki.find(doc, "#confirm-email-form")
assert [conf] = Floki.find(doc, ".form__confirmation")
assert Floki.text(conf) =~ ~r/mailed you/
end
end
describe "confirmation" do
test "must be a guest" do
end
test "not found" do
conn = conn()
conn = get(conn, "/confirm-email/#{Fake.confirm_token()}")
doc = floki_response(conn)
assert [form] = Floki.find(doc, "#confirm-email-form")
assert [_] = Floki.find(form, "#confirm-email-form_email")
assert [_] = Floki.find(form, "button[type='submit']")
assert [err] = Floki.find(doc, ".error")
assert Floki.text(err) =~ ~r/invalid confirmation link/i
end
test "success" do
conn = conn()
account = fake_account!()
conn = get(conn, "/confirm-email/#{account.email.confirm_token}")
assert redirected_to(conn) == "/home"
end
test "twice confirm" do
conn = conn()
account = fake_account!()
conn = get(conn, "/confirm-email/#{account.email.confirm_token}")
assert redirected_to(conn) == "/home"
conn = get(build_conn(), "/confirm-email/#{account.email.confirm_token}")
doc = floki_response(conn)
assert [form] = Floki.find(doc, "#confirm-email-form")
assert [_] = Floki.find(form, "#confirm-email-form_email")
assert [_] = Floki.find(form, "button[type='submit']")
assert [err] = Floki.find(doc, ".error")
assert Floki.text(err) =~ ~r/invalid confirmation link/i
end
end
end

View file

@ -0,0 +1,135 @@
defmodule VoxPublica.Web.CreateUserController.Test do
use VoxPublica.ConnCase
test "form renders" do
alice = fake_account!()
conn = conn(account: alice)
conn = get(conn, "/create-user")
doc = floki_response(conn)
assert [form] = Floki.find(doc, "#create-form")
assert [_] = Floki.find(form, "#create-form_username")
assert [_] = Floki.find(form, "#create-form_name")
assert [_] = Floki.find(form, "#create-form_summary")
assert [_] = Floki.find(form, "button[type='submit']")
end
describe "required fields" do
test "missing all" do
alice = fake_account!()
conn = conn(account: alice)
conn = post(conn, "/create-user", %{"create_form" => %{}})
doc = floki_response(conn)
assert [form] = Floki.find(doc, "#create-form")
assert [_] = Floki.find(form, "#create-form_username")
assert_field_error(form, "create-form_username", ~r/can't be blank/)
assert [_] = Floki.find(form, "#create-form_name")
assert_field_error(form, "create-form_name", ~r/can't be blank/)
assert [_] = Floki.find(form, "#create-form_summary")
assert_field_error(form, "create-form_summary", ~r/can't be blank/)
assert [_] = Floki.find(form, "button[type='submit']")
end
test "with name" do
alice = fake_account!()
conn = conn(account: alice)
conn = post(conn, "/create-user", %{"create_form" => %{"name" => Fake.name()}})
doc = floki_response(conn)
assert [form] = Floki.find(doc, "#create-form")
assert_field_good(form, "create-form_name")
assert_field_error(form, "create-form_username", ~r/can't be blank/)
assert_field_error(form, "create-form_summary", ~r/can't be blank/)
assert [_] = Floki.find(form, "button[type='submit']")
end
test "with username" do
alice = fake_account!()
conn = conn(account: alice)
conn = post(conn, "/create-user", %{"create_form" => %{"username" => Fake.username()}})
doc = floki_response(conn)
assert [form] = Floki.find(doc, "#create-form")
assert_field_good(form, "create-form_username")
assert_field_error(form, "create-form_name", ~r/can't be blank/)
assert_field_error(form, "create-form_summary", ~r/can't be blank/)
assert [_] = Floki.find(form, "button[type='submit']")
end
test "with summary" do
alice = fake_account!()
conn = conn(account: alice)
conn = post(conn, "/create-user", %{"create_form" => %{"summary" => Fake.summary()}})
doc = floki_response(conn)
assert [form] = Floki.find(doc, "#create-form")
assert_field_good(form, "create-form_summary")
assert_field_error(form, "create-form_username", ~r/can't be blank/)
assert_field_error(form, "create-form_name", ~r/can't be blank/)
assert [_] = Floki.find(form, "button[type='submit']")
end
test "missing username" do
alice = fake_account!()
conn = conn(account: alice)
conn = post(conn, "/create-user", %{"create_form" => %{"summary" => Fake.summary(), "name" => Fake.name()}})
doc = floki_response(conn)
assert [form] = Floki.find(doc, "#create-form")
assert_field_good(form, "create-form_summary")
assert_field_good(form, "create-form_name")
assert_field_error(form, "create-form_username", ~r/can't be blank/)
assert [_] = Floki.find(form, "button[type='submit']")
end
test "missing name" do
alice = fake_account!()
conn = conn(account: alice)
conn = post(conn, "/create-user", %{"create_form" => %{"summary" => Fake.summary(), "username" => Fake.username()}})
doc = floki_response(conn)
assert [form] = Floki.find(doc, "#create-form")
assert_field_good(form, "create-form_summary")
assert_field_good(form, "create-form_username")
assert_field_error(form, "create-form_name", ~r/can't be blank/)
assert [_] = Floki.find(form, "button[type='submit']")
end
test "missing summary" do
alice = fake_account!()
conn = conn(account: alice)
conn = post(conn, "/create-user", %{"create_form" => %{"name" => Fake.name(), "username" => Fake.username()}})
doc = floki_response(conn)
assert [form] = Floki.find(doc, "#create-form")
assert_field_good(form, "create-form_username")
assert_field_good(form, "create-form_name")
assert_field_error(form, "create-form_summary", ~r/can't be blank/)
assert [_] = Floki.find(form, "button[type='submit']")
end
end
test "username taken" do
alice = fake_account!()
user = fake_user!(alice)
conn = conn(account: alice)
params = %{"create_form" => %{"summary" => Fake.summary(), "name" => Fake.name(), "username" => user.character.username}}
conn = post(conn, "/create-user", params)
doc = floki_response(conn)
assert [form] = Floki.find(doc, "#create-form")
assert_field_good(form, "create-form_summary")
assert_field_good(form, "create-form_name")
assert_field_error(form, "create-form_username", ~r/has already been taken/)
assert [_] = Floki.find(form, "button[type='submit']")
end
test "success" do
alice = fake_account!()
conn = conn(account: alice)
username = Fake.username()
params = %{"create_form" => %{"summary" => Fake.summary(), "name" => Fake.name(), "username" => username}}
conn = post(conn, "/create-user", params)
assert redirected_to(conn) == "/home/@#{username}"
conn = get(recycle(conn), "/home/@#{username}")
doc = floki_response(conn)
assert [err] = find_flash(doc)
assert_flash(err, :info, ~r/all ready/)
end
end

View file

@ -0,0 +1,100 @@
defmodule VoxPublica.Web.ForgotPasswordController.Test do
use VoxPublica.ConnCase
# alias VoxPublica.Accounts
# test "form renders" do
# conn = conn()
# conn = get(conn, "/login")
# doc = floki_response(conn)
# assert [form] = Floki.find(doc, "#login-form")
# assert [_] = Floki.find(form, "#login-form_email")
# assert [_] = Floki.find(form, "#login-form_password")
# assert [_] = Floki.find(form, "button[type='submit']")
# end
# describe "required fields" do
# test "missing both" do
# conn = conn()
# conn = post(conn, "/login", %{"login_form" => %{}})
# doc = floki_response(conn)
# assert [form] = Floki.find(doc, "#login-form")
# assert [_] = Floki.find(form, "#login-form_email")
# assert [email_error] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='login-form_email']")
# assert "can't be blank" == Floki.text(email_error)
# assert [_] = Floki.find(form, "#login-form_password")
# assert [password_error] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='login-form_password']")
# assert "can't be blank" == Floki.text(password_error)
# assert [_] = Floki.find(form, "button[type='submit']")
# end
# test "missing password" do
# conn = conn()
# email = Fake.email()
# conn = post(conn, "/login", %{"login_form" => %{"email" => email}})
# doc = floki_response(conn)
# assert [form] = Floki.find(doc, "#login-form")
# assert [_] = Floki.find(form, "#login-form_email")
# assert [] == Floki.find(form, "span.invalid-feedback[phx-feedback-for='login-form_email']")
# assert [_] = Floki.find(form, "#login-form_password")
# assert [password_error] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='login-form_password']")
# assert "can't be blank" == Floki.text(password_error)
# assert [_] = Floki.find(form, "button[type='submit']")
# end
# test "missing email" do
# conn = conn()
# password = Fake.password()
# conn = post(conn, "/login", %{"login_form" => %{"password" => password}})
# doc = floki_response(conn)
# assert [form] = Floki.find(doc, "#login-form")
# assert [_] = Floki.find(form, "#login-form_email")
# assert [email_error] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='login-form_email']")
# assert [_] = Floki.find(form, "#login-form_password")
# assert [] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='login-form_password']")
# assert "can't be blank" == Floki.text(email_error)
# assert [_] = Floki.find(form, "button[type='submit']")
# end
# end
# test "not found" do
# conn = conn()
# email = Fake.email()
# password = Fake.password()
# params = %{"login_form" => %{"email" => email, "password" => password}}
# conn = post(conn, "/login", params)
# doc = floki_response(conn)
# assert [div] = Floki.find(doc, "div.box__warning")
# assert [span] = Floki.find(div, "span")
# assert Floki.text(span) =~ ~r/incorrect/
# assert [_] = Floki.find(doc, "#login-form")
# end
# test "not activated" do
# conn = conn()
# account = fake_account!()
# params = %{"login_form" =>
# %{"email" => account.email.email,
# "password" => account.login_credential.password}}
# conn = post(conn, "/login", params)
# doc = floki_response(conn)
# assert [div] = Floki.find(doc, "div.box__warning")
# assert [span] = Floki.find(div, "span")
# assert Floki.text(span) =~ ~r/confirm/
# assert [_] = Floki.find(doc, "#login-form")
# end
# test "success" do
# conn = conn()
# account = fake_account!()
# {:ok, account} = Accounts.confirm_email(account)
# params = %{"login_form" =>
# %{"email" => account.email.email,
# "password" => account.login_credential.password}}
# conn = post(conn, "/login", params)
# assert redirected_to(conn) == "/home"
# end
end

View file

@ -0,0 +1,100 @@
defmodule VoxPublica.Web.LoginController.Test do
use VoxPublica.ConnCase
alias VoxPublica.Accounts
test "form renders" do
conn = conn()
conn = get(conn, "/login")
doc = floki_response(conn)
assert [form] = Floki.find(doc, "#login-form")
assert [_] = Floki.find(form, "#login-form_email")
assert [_] = Floki.find(form, "#login-form_password")
assert [_] = Floki.find(form, "button[type='submit']")
end
describe "required fields" do
test "missing both" do
conn = conn()
conn = post(conn, "/login", %{"login_form" => %{}})
doc = floki_response(conn)
assert [form] = Floki.find(doc, "#login-form")
assert [_] = Floki.find(form, "#login-form_email")
assert [email_error] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='login-form_email']")
assert "can't be blank" == Floki.text(email_error)
assert [_] = Floki.find(form, "#login-form_password")
assert [password_error] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='login-form_password']")
assert "can't be blank" == Floki.text(password_error)
assert [_] = Floki.find(form, "button[type='submit']")
end
test "missing password" do
conn = conn()
email = Fake.email()
conn = post(conn, "/login", %{"login_form" => %{"email" => email}})
doc = floki_response(conn)
assert [form] = Floki.find(doc, "#login-form")
assert [_] = Floki.find(form, "#login-form_email")
assert [] == Floki.find(form, "span.invalid-feedback[phx-feedback-for='login-form_email']")
assert [_] = Floki.find(form, "#login-form_password")
assert [password_error] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='login-form_password']")
assert "can't be blank" == Floki.text(password_error)
assert [_] = Floki.find(form, "button[type='submit']")
end
test "missing email" do
conn = conn()
password = Fake.password()
conn = post(conn, "/login", %{"login_form" => %{"password" => password}})
doc = floki_response(conn)
assert [form] = Floki.find(doc, "#login-form")
assert [_] = Floki.find(form, "#login-form_email")
assert [email_error] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='login-form_email']")
assert [_] = Floki.find(form, "#login-form_password")
assert [] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='login-form_password']")
assert "can't be blank" == Floki.text(email_error)
assert [_] = Floki.find(form, "button[type='submit']")
end
end
test "not found" do
conn = conn()
email = Fake.email()
password = Fake.password()
params = %{"login_form" => %{"email" => email, "password" => password}}
conn = post(conn, "/login", params)
doc = floki_response(conn)
assert [div] = Floki.find(doc, "div.box__warning")
assert [span] = Floki.find(div, "span")
assert Floki.text(span) =~ ~r/incorrect/
assert [_] = Floki.find(doc, "#login-form")
end
test "not activated" do
conn = conn()
account = fake_account!()
params = %{"login_form" =>
%{"email" => account.email.email,
"password" => account.login_credential.password}}
conn = post(conn, "/login", params)
doc = floki_response(conn)
assert [div] = Floki.find(doc, "div.box__warning")
assert [span] = Floki.find(div, "span")
assert Floki.text(span) =~ ~r/confirm/
assert [_] = Floki.find(doc, "#login-form")
end
test "success" do
conn = conn()
account = fake_account!()
{:ok, account} = Accounts.confirm_email(account)
params = %{"login_form" =>
%{"email" => account.email.email,
"password" => account.login_credential.password}}
conn = post(conn, "/login", params)
assert redirected_to(conn) == "/home"
end
end

View file

@ -0,0 +1,95 @@
defmodule VoxPublica.Web.ResetPasswordController.Test do
use VoxPublica.ConnCase
# alias VoxPublica.Fake
# describe "request" do
# test "form renders" do
# conn = conn()
# conn = get(conn, "/confirm-email")
# doc = floki_response(conn)
# assert [form] = Floki.find(doc, "#confirm-email-form")
# assert [_] = Floki.find(form, "#confirm-email-form_email")
# assert [_] = Floki.find(form, "button[type='submit']")
# assert [] = Floki.find(doc, ".error")
# end
# test "absence validation" do
# conn = conn()
# conn = post(conn, "/confirm-email", %{})
# doc = floki_response(conn)
# assert [form] = Floki.find(doc, "#confirm-email-form")
# assert [_] = Floki.find(form, "#confirm-email-form_email")
# assert [_] = Floki.find(form, "button[type='submit']")
# assert [] = Floki.find(doc, ".error")
# assert [err] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='confirm-email-form_email']")
# assert "can't be blank" == Floki.text(err)
# end
# test "format validation" do
# conn = conn()
# conn = post(conn, "/confirm-email", %{"confirm_email_form" => %{"email" => Faker.Pokemon.name()}})
# doc = floki_response(conn)
# assert [form] = Floki.find(doc, "#confirm-email-form")
# assert [_] = Floki.find(form, "#confirm-email-form_email")
# assert [_] = Floki.find(form, "button[type='submit']")
# assert [] = Floki.find(doc, ".error")
# assert [err] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='confirm-email-form_email']")
# assert "has invalid format" == Floki.text(err)
# end
# test "not found" do
# conn = conn()
# conn = post(conn, "/confirm-email", %{"confirm_email_form" => %{"email" => Fake.email()}})
# doc = floki_response(conn)
# assert [form] = Floki.find(doc, "#confirm-email-form")
# assert [_] = Floki.find(form, "#confirm-email-form_email")
# assert [_] = Floki.find(form, "button[type='submit']")
# assert [err] = Floki.find(doc, ".error")
# assert Floki.text(err) =~ ~r/invalid confirmation link/i
# end
# # TODO
# # test "expired" do
# # conn = conn()
# # end
# end
# describe "confirmation" do
# test "not found" do
# conn = conn()
# conn = get(conn, "/confirm-email/#{Fake.confirm_token()}")
# doc = floki_response(conn)
# assert [form] = Floki.find(doc, "#confirm-email-form")
# assert [_] = Floki.find(form, "#confirm-email-form_email")
# assert [_] = Floki.find(form, "button[type='submit']")
# assert [err] = Floki.find(doc, ".error")
# assert Floki.text(err) =~ ~r/invalid confirmation link/i
# end
# test "success" do
# conn = conn()
# account = fake_account!()
# conn = get(conn, "/confirm-email/#{account.email.confirm_token}")
# assert redirected_to(conn) == "/home"
# end
# test "twice confirm" do
# conn = conn()
# account = fake_account!()
# conn = get(conn, "/confirm-email/#{account.email.confirm_token}")
# assert redirected_to(conn) == "/home"
# conn = get(build_conn(), "/confirm-email/#{account.email.confirm_token}")
# doc = floki_response(conn)
# assert [form] = Floki.find(doc, "#confirm-email-form")
# assert [_] = Floki.find(form, "#confirm-email-form_email")
# assert [_] = Floki.find(form, "button[type='submit']")
# assert [err] = Floki.find(doc, ".error")
# assert Floki.text(err) =~ ~r/invalid confirmation link/i
# end
# end
end

View file

@ -0,0 +1,72 @@
defmodule VoxPublica.Web.SignupController.Test do
use VoxPublica.ConnCase
test "form renders" do
conn = conn()
conn = get(conn, "/signup")
doc = floki_response(conn)
assert [form] = Floki.find(doc, "#signup-form")
assert [_] = Floki.find(form, "#signup-form_email")
assert [_] = Floki.find(form, "#signup-form_password")
assert [_] = Floki.find(form, "button[type='submit']")
end
describe "required fields" do
test "missing both" do
conn = conn()
conn = post(conn, "/signup", %{"signup_form" => %{}})
doc = floki_response(conn)
assert [form] = Floki.find(doc, "#signup-form")
assert [_] = Floki.find(form, "#signup-form_email")
assert [email_error] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='signup-form_email']")
assert "can't be blank" == Floki.text(email_error)
assert [_] = Floki.find(form, "#signup-form_password")
assert [password_error] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='signup-form_password']")
assert "can't be blank" == Floki.text(password_error)
assert [_] = Floki.find(form, "button[type='submit']")
end
test "missing password" do
conn = conn()
email = Fake.email()
conn = post(conn, "/signup", %{"signup_form" => %{"email" => email}})
doc = floki_response(conn)
assert [form] = Floki.find(doc, "#signup-form")
assert [_] = Floki.find(form, "#signup-form_email")
assert [] == Floki.find(form, "span.invalid-feedback[phx-feedback-for='signup-form_email']")
assert [_] = Floki.find(form, "#signup-form_password")
assert [password_error] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='signup-form_password']")
assert "can't be blank" == Floki.text(password_error)
assert [_] = Floki.find(form, "button[type='submit']")
end
test "missing email" do
conn = conn()
password = Fake.password()
conn = post(conn, "/signup", %{"signup_form" => %{"password" => password}})
doc = floki_response(conn)
assert [form] = Floki.find(doc, "#signup-form")
assert [_] = Floki.find(form, "#signup-form_email")
assert [email_error] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='signup-form_email']")
assert [_] = Floki.find(form, "#signup-form_password")
assert [] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='signup-form_password']")
assert "can't be blank" == Floki.text(email_error)
assert [_] = Floki.find(form, "button[type='submit']")
end
end
test "success" do
conn = conn()
email = Fake.email()
password = Fake.password()
conn = post(conn, "/signup", %{"signup_form" => %{"email" => email, "password" => password}})
doc = floki_response(conn)
assert [div] = Floki.find(doc, "div.form__confirmation")
assert [p] = Floki.find(div, "p")
assert Floki.text(p) =~ ~r/mailed.+you a link/s
assert [] = Floki.find(doc, "#signup-form")
end
end

View file

@ -0,0 +1,96 @@
defmodule VoxPublica.Web.SwitchUserController.Test do
use VoxPublica.ConnCase
alias VoxPublica.Fake
describe "index" do
test "not logged in" do
conn = conn()
conn = get(conn, "/switch-user")
assert redirected_to(conn) == "/login"
conn = get(recycle(conn), "/login")
doc = floki_response(conn)
assert [err] = find_flash(doc)
assert_flash(err, :error, ~r/must log in/)
end
test "no users" do
account = fake_account!()
conn = conn(account: account)
conn = get(conn, "/switch-user")
assert redirected_to(conn) == "/create-user"
conn = get(recycle(conn), "/create-user")
doc = floki_response(conn)
assert [err] = find_flash(doc)
assert_flash(err, :info, ~r/fill out/)
end
test "shows users" do
account = fake_account!()
alice = fake_user!(account)
bob = fake_user!(account)
conn = conn(account: account)
conn = get(conn, "/switch-user")
doc = floki_response(conn)
[a, b] = Floki.find(doc, "ul.user-list li")
assert Floki.text(a) == "@#{alice.character.username}"
assert Floki.text(b) == "@#{bob.character.username}"
end
end
describe "show" do
test "not logged in" do
conn = conn()
conn = get(conn, "/switch-user/@#{Fake.username()}")
assert redirected_to(conn) == "/login"
conn = get(recycle(conn), "/login")
doc = floki_response(conn)
assert [err] = find_flash(doc)
assert_flash(err, :error, ~r/must log in/)
end
test "not found" do
account = fake_account!()
_user = fake_user!(account)
conn = conn(account: account)
conn = get(conn, "/switch-user/@#{Fake.username()}")
assert redirected_to(conn) == "/switch-user"
conn = get(recycle(conn), "/switch-user")
doc = floki_response(conn)
assert [err] = find_flash(doc)
assert_flash(err, :error, ~r/does not exist/)
end
test "not permitted" do
alice = fake_account!()
bob = fake_account!()
_alice_user = fake_user!(alice)
bob_user = fake_user!(bob)
conn = conn(account: alice)
conn = get(conn, "/switch-user/@#{bob_user.character.username}")
assert redirected_to(conn) == "/switch-user"
conn = get(recycle(conn), "/switch-user")
doc = floki_response(conn)
assert [err] = find_flash(doc)
assert_flash(err, :error, ~r/not permitted/)
end
test "success" do
account = fake_account!()
user = fake_user!(account)
conn = conn(account: account)
conn = get(conn, "/switch-user/@#{user.character.username}")
assert redirected_to(conn) == "/home/@#{user.character.username}"
conn = get(conn, "/home/@#{user.character.username}")
assert get_session(conn, :user_id) == user.id
doc = floki_response(conn)
assert [err] = find_flash(doc)
assert_flash(err, :info, "Welcome back, @#{user.character.username}!")
end
end
end

View file

@ -1,81 +0,0 @@
defmodule VoxPublica.Web.LoginLive.Test do
use VoxPublica.ConnCase
alias VoxPublica.Accounts
test "disconnected", %{conn: conn} do
conn = get(conn, "/login")
doc = floki_response(conn)
assert [form] = Floki.find(doc, "#login-form")
assert [_] = Floki.find(form, "#login-form_email")
assert [_] = Floki.find(form, "#login-form_password")
assert [_] = Floki.find(form, "button[type='submit']")
end
test "required fields", %{conn: conn} do
{view, doc} = floki_live(conn, "/login")
assert [form] = Floki.find(doc, "#login-form")
assert [email_input] = Floki.find(form, "#login-form_email")
assert [password_input] = Floki.find(form, "#login-form_password")
assert [submit] = Floki.find(form, "button[type='submit']")
doc = floki_submit(view, :submit, %{})
assert [form] = Floki.find(doc, "#login-form")
assert [_] = Floki.find(form, "#login-form_email")
assert [email_error] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='login-form_email']")
assert "can't be blank" == Floki.text(email_error)
assert [_] = Floki.find(form, "#login-form_password")
assert [password_error] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='login-form_password']")
assert "can't be blank" == Floki.text(password_error)
assert [_] = Floki.find(form, "button[type='submit']")
email = Fake.email()
password = Fake.password()
doc = floki_submit(view, :submit, %{"email" => email})
assert [form] = Floki.find(doc, "#login-form")
assert [_] = Floki.find(form, "#login-form_email")
assert [] == Floki.find(form, "span.invalid-feedback[phx-feedback-for='login-form_email']")
assert [_] = Floki.find(form, "#login-form_password")
assert [password_error] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='login-form_password']")
assert "can't be blank" == Floki.text(password_error)
assert [_] = Floki.find(form, "button[type='submit']")
doc = floki_submit(view, :submit, %{"password" => password})
assert [form] = Floki.find(doc, "#login-form")
assert [_] = Floki.find(form, "#login-form_email")
assert [_] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='login-form_email']")
assert [_] = Floki.find(form, "#login-form_password")
assert [] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='login-form_password']")
assert "can't be blank" == Floki.text(password_error)
assert [_] = Floki.find(form, "button[type='submit']")
end
test "not found", %{conn: conn} do
{view, _} = floki_live(conn, "/login")
email = Fake.email()
password = Fake.password()
doc = floki_submit(view, :submit, %{"email" => email, "password" => password})
assert [div] = Floki.find(doc, "div.error")
assert [span] = Floki.find(div, "span")
assert Floki.text(span) =~ ~r/incorrect/
assert [] = Floki.find(doc, "#register-form")
end
test "not activated", %{conn: conn} do
{view, _} = floki_live(conn, "/login")
{:ok, account} = Accounts.register(Fake.account())
params = %{"email" => account.email.email, "password" => account.login_credential.password}
doc = floki_submit(view, :submit, params)
assert [div] = Floki.find(doc, "div.error")
assert [span] = Floki.find(div, "span")
assert Floki.text(span) =~ ~r/confirm/
assert [_] = Floki.find(doc, "#login-form")
end
test "success", %{conn: conn} do
{view, _} = floki_live(conn, "/login")
{:ok, account} = Accounts.register(Fake.account())
{:ok, account} = Accounts.confirm_email(account)
params = %{"email" => account.email.email, "password" => account.login_credential.password}
assert {:error, {:live_redirect, %{kind: :push, to: "/home"}}} == render_submit(view, :submit, params)
end
end

View file

@ -1,60 +0,0 @@
defmodule VoxPublica.Web.RegisterLive.Test do
use VoxPublica.ConnCase
test "disconnected", %{conn: conn} do
conn = get(conn, "/register")
doc = floki_response(conn)
assert [form] = Floki.find(doc, "#register-form")
assert [_] = Floki.find(form, "#register-form_email")
assert [_] = Floki.find(form, "#register-form_password")
assert [_] = Floki.find(form, "button[type='submit']")
end
test "required fields", %{conn: conn} do
{view, doc} = floki_live(conn, "/register")
assert [form] = Floki.find(doc, "#register-form")
assert [email_input] = Floki.find(form, "#register-form_email")
assert [password_input] = Floki.find(form, "#register-form_password")
assert [submit] = Floki.find(form, "button[type='submit']")
doc = floki_submit(view, :submit, %{})
assert [form] = Floki.find(doc, "#register-form")
assert [_] = Floki.find(form, "#register-form_email")
assert [email_error] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='register-form_email']")
assert "can't be blank" == Floki.text(email_error)
assert [_] = Floki.find(form, "#register-form_password")
assert [password_error] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='register-form_password']")
assert "can't be blank" == Floki.text(password_error)
assert [_] = Floki.find(form, "button[type='submit']")
email = Fake.email()
password = Fake.password()
doc = floki_submit(view, :submit, %{"email" => email})
assert [form] = Floki.find(doc, "#register-form")
assert [_] = Floki.find(form, "#register-form_email")
assert [] == Floki.find(form, "span.invalid-feedback[phx-feedback-for='register-form_email']")
assert [_] = Floki.find(form, "#register-form_password")
assert [password_error] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='register-form_password']")
assert "can't be blank" == Floki.text(password_error)
assert [_] = Floki.find(form, "button[type='submit']")
doc = floki_submit(view, :submit, %{"password" => password})
assert [form] = Floki.find(doc, "#register-form")
assert [_] = Floki.find(form, "#register-form_email")
assert [_] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='register-form_email']")
assert [_] = Floki.find(form, "#register-form_password")
assert [] = Floki.find(form, "span.invalid-feedback[phx-feedback-for='register-form_password']")
assert "can't be blank" == Floki.text(password_error)
assert [_] = Floki.find(form, "button[type='submit']")
end
test "success", %{conn: conn} do
{view, _} = floki_live(conn, "/register")
email = Fake.email()
password = Fake.password()
doc = floki_submit(view, :submit, %{"email" => email, "password" => password})
assert [div] = Floki.find(doc, "div.info")
assert [span] = Floki.find(div, "span")
assert Floki.text(span) =~ ~r/mailed.+you a link/s
assert [] = Floki.find(doc, "#register-form")
end
end