mirror of
https://github.com/bonfire-networks/bonfire-app.git
synced 2024-05-16 08:02:40 +00:00
Merge branch 'main' of github.com:jjl/vox_publica into main
This commit is contained in:
commit
6613ef3a6e
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -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
2
.tool-versions
Normal file
|
@ -0,0 +1,2 @@
|
|||
erlang 23.0.4
|
||||
elixir 1.10.4-otp-23
|
13
Makefile
Normal file
13
Makefile
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
14
deps.git
Normal 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
36
deps.hex
Normal 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"
|
|
@ -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
|
||||
|
|
24
lib/accounts/change_password_form.ex
Normal file
24
lib/accounts/change_password_form.ex
Normal 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
|
21
lib/accounts/confirm_email_form.ex
Normal file
21
lib/accounts/confirm_email_form.ex
Normal 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
29
lib/accounts/emails.ex
Normal 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
|
23
lib/accounts/reset_password_form.ex
Normal file
23
lib/accounts/reset_password_form.ex
Normal 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
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
31
lib/mailer.ex
Normal 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
|
19
lib/repo.ex
19
lib/repo.ex
|
@ -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
|
||||
|
|
45
lib/users.ex
45
lib/users.ex
|
@ -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
32
lib/users/create_form.ex
Normal 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
85
lib/users/users.ex
Normal 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
12
lib/utils.ex
Normal 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
215
lib/web/common_helper.ex
Normal 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
|
12
lib/web/controllers/change_password_controller.ex
Normal file
12
lib/web/controllers/change_password_controller.ex
Normal 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
|
53
lib/web/controllers/confirm_email_controller.ex
Normal file
53
lib/web/controllers/confirm_email_controller.ex
Normal 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
|
31
lib/web/controllers/create_user_controller.ex
Normal file
31
lib/web/controllers/create_user_controller.ex
Normal 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
|
12
lib/web/controllers/forgot_password_controller.ex
Normal file
12
lib/web/controllers/forgot_password_controller.ex
Normal 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
|
30
lib/web/controllers/login_controller.ex
Normal file
30
lib/web/controllers/login_controller.ex
Normal 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
|
12
lib/web/controllers/reset_password_controller.ex
Normal file
12
lib/web/controllers/reset_password_controller.ex
Normal 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
|
30
lib/web/controllers/signup_controller.ex
Normal file
30
lib/web/controllers/signup_controller.ex
Normal 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
|
61
lib/web/controllers/switch_user_controller.ex
Normal file
61
lib/web/controllers/switch_user_controller.ex
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -1,7 +0,0 @@
|
|||
<form>
|
||||
<label>
|
||||
Username: <input type="text" />
|
||||
Password: <input type="text" />
|
||||
<button type="submit" phx-click="register">Register</button>
|
||||
</label>
|
||||
</form>
|
34
lib/web/live/home/home_live.ex
Normal file
34
lib/web/live/home/home_live.ex
Normal 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
|
25
lib/web/live/home/home_live.html.leex
Normal file
25
lib/web/live/home/home_live.html.leex
Normal 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>
|
34
lib/web/live/index/index_live.ex
Normal file
34
lib/web/live/index/index_live.ex
Normal 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
|
25
lib/web/live/index/index_live.html.leex
Normal file
25
lib/web/live/index/index_live.html.leex
Normal 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>
|
|
@ -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
|
|
@ -1 +0,0 @@
|
|||
Home.
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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>
|
|
@ -1,5 +0,0 @@
|
|||
defmodule VoxPublica.Web.ChangePasswordLive do
|
||||
use VoxPublica.Web, :live_view
|
||||
use Phoenix.HTML
|
||||
|
||||
end
|
|
@ -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>
|
|
@ -1,6 +0,0 @@
|
|||
defmodule VoxPublica.Web.ResetPasswordLive do
|
||||
use VoxPublica.Web, :live_view
|
||||
use Phoenix.HTML
|
||||
|
||||
|
||||
end
|
|
@ -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(
|
||||
|
|
|
@ -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
|
|
@ -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>
|
||||
|
||||
|
||||
|
21
lib/web/plugs/must_be_guest.ex
Normal file
21
lib/web/plugs/must_be_guest.ex
Normal 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
|
39
lib/web/plugs/must_log_in.ex
Normal file
39
lib/web/plugs/must_log_in.ex
Normal 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
|
|
@ -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,
|
||||
|
|
14
lib/web/templates/change_password/form.html.eex
Normal file
14
lib/web/templates/change_password/form.html.eex
Normal 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>
|
44
lib/web/templates/confirm_email/form.html.eex
Normal file
44
lib/web/templates/confirm_email/form.html.eex
Normal 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>👋 </span>
|
||||
Already confirmed your email?
|
||||
<%= link "Log in", to: Routes.login_path(@conn, :index) %>.
|
||||
</p>
|
||||
<p>
|
||||
<span>🧐 </span>
|
||||
Don't have an account yet?
|
||||
<%= link "Create a new one", to: Routes.signup_path(@conn, :index) %>
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
36
lib/web/templates/create_user/form.html.eex
Normal file
36
lib/web/templates/create_user/form.html.eex
Normal 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>
|
0
lib/web/templates/email/confirm.text.eex
Normal file
0
lib/web/templates/email/confirm.text.eex
Normal 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>
|
||||
|
|
18
lib/web/templates/layout/header/header_live.ex
Normal file
18
lib/web/templates/layout/header/header_live.ex
Normal 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
|
57
lib/web/templates/layout/header/header_live.html.leex
Normal file
57
lib/web/templates/layout/header/header_live.html.leex
Normal 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>
|
|
@ -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>
|
||||
|
|
70
lib/web/templates/login/form.html.eex
Normal file
70
lib/web/templates/login/form.html.eex
Normal 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>👋</span>
|
||||
Don't have an account yet?
|
||||
<%= link "Create a new one.", to: Routes.signup_path(@conn, :index) %>
|
||||
</p>
|
||||
<p>
|
||||
<span>🧐</span>
|
||||
Trouble logging in?
|
||||
<%= link "Reset your password.", to: Routes.forgot_password_path(@conn, :index) %>.
|
||||
</p>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
71
lib/web/templates/signup/form.html.eex
Normal file
71
lib/web/templates/signup/form.html.eex
Normal 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>👋 </span>
|
||||
Do you already have an account?
|
||||
<%= link "Log in", to: "/login" %>.
|
||||
</p>
|
||||
</aside>
|
||||
</div>
|
||||
<% end %>
|
||||
</section>
|
||||
</main>
|
10
lib/web/templates/switch_user/list.html.eex
Normal file
10
lib/web/templates/switch_user/list.html.eex
Normal 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>
|
3
lib/web/views/change_password_view.ex
Normal file
3
lib/web/views/change_password_view.ex
Normal file
|
@ -0,0 +1,3 @@
|
|||
defmodule VoxPublica.Web.ChangePasswordView do
|
||||
use VoxPublica.Web, :view
|
||||
end
|
3
lib/web/views/confirm_email_view.ex
Normal file
3
lib/web/views/confirm_email_view.ex
Normal file
|
@ -0,0 +1,3 @@
|
|||
defmodule VoxPublica.Web.ConfirmEmailView do
|
||||
use VoxPublica.Web, :view
|
||||
end
|
3
lib/web/views/create_user_view.ex
Normal file
3
lib/web/views/create_user_view.ex
Normal file
|
@ -0,0 +1,3 @@
|
|||
defmodule VoxPublica.Web.CreateUserView do
|
||||
use VoxPublica.Web, :view
|
||||
end
|
3
lib/web/views/email_view.ex
Normal file
3
lib/web/views/email_view.ex
Normal file
|
@ -0,0 +1,3 @@
|
|||
defmodule VoxPublica.Web.EmailView do
|
||||
use VoxPublica.Web, :view
|
||||
end
|
3
lib/web/views/login_view.ex
Normal file
3
lib/web/views/login_view.ex
Normal file
|
@ -0,0 +1,3 @@
|
|||
defmodule VoxPublica.Web.LoginView do
|
||||
use VoxPublica.Web, :view
|
||||
end
|
3
lib/web/views/reset_password_view.ex
Normal file
3
lib/web/views/reset_password_view.ex
Normal file
|
@ -0,0 +1,3 @@
|
|||
defmodule VoxPublica.Web.ResetPasswordView do
|
||||
use VoxPublica.Web, :view
|
||||
end
|
3
lib/web/views/signup_view.ex
Normal file
3
lib/web/views/signup_view.ex
Normal file
|
@ -0,0 +1,3 @@
|
|||
defmodule VoxPublica.Web.SignupView do
|
||||
use VoxPublica.Web, :view
|
||||
end
|
3
lib/web/views/switch_user_view.ex
Normal file
3
lib/web/views/switch_user_view.ex
Normal file
|
@ -0,0 +1,3 @@
|
|||
defmodule VoxPublica.Web.SwitchUserView do
|
||||
use VoxPublica.Web, :view
|
||||
end
|
|
@ -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
38
mess.exs
Normal 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
94
mix.exs
|
@ -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
|
||||
|
|
19
mix.lock
19
mix.lock
|
@ -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"},
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
6
test/support/data_helpers.ex
Normal file
6
test/support/data_helpers.ex
Normal file
|
@ -0,0 +1,6 @@
|
|||
defmodule VoxPublica.DataHelpers do
|
||||
|
||||
# import ExUnit.Assertions
|
||||
# alias VoxPublica.Fake
|
||||
|
||||
end
|
18
test/support/fake_helpers.ex
Normal file
18
test/support/fake_helpers.ex
Normal 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
|
|
@ -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
|
||||
|
|
100
test/web/controllers/change_password_test.exs
Normal file
100
test/web/controllers/change_password_test.exs
Normal 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
|
116
test/web/controllers/confirm_email_test.exs
Normal file
116
test/web/controllers/confirm_email_test.exs
Normal 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
|
135
test/web/controllers/create_user_test.exs
Normal file
135
test/web/controllers/create_user_test.exs
Normal 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
|
100
test/web/controllers/forgot_password_test.exs
Normal file
100
test/web/controllers/forgot_password_test.exs
Normal 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
|
100
test/web/controllers/login_test.exs
Normal file
100
test/web/controllers/login_test.exs
Normal 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
|
95
test/web/controllers/reset_password_test.exs
Normal file
95
test/web/controllers/reset_password_test.exs
Normal 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
|
72
test/web/controllers/signup_test.exs
Normal file
72
test/web/controllers/signup_test.exs
Normal 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
|
96
test/web/controllers/switch_user_test.exs
Normal file
96
test/web/controllers/switch_user_test.exs
Normal 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
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in a new issue