admin users and roles

This commit is contained in:
byte[] 2019-12-15 21:21:14 -05:00
parent f5996dc084
commit f8431cb1c2
14 changed files with 348 additions and 10 deletions

View file

@ -9,6 +9,7 @@ defmodule Philomena.Users do
alias Philomena.Users.Uploader
alias Philomena.Users.User
alias Philomena.Roles.Role
use Pow.Ecto.Context,
repo: Repo,
@ -56,6 +57,7 @@ defmodule Philomena.Users do
"""
def create_user(attrs \\ %{}) do
roles =
%User{}
|> User.changeset(attrs)
|> Repo.insert()
@ -74,11 +76,19 @@ defmodule Philomena.Users do
"""
def update_user(%User{} = user, attrs) do
roles =
Role
|> where([r], r.id in ^clean_roles(attrs["roles"]))
|> Repo.all()
user
|> User.changeset(attrs)
|> User.update_changeset(attrs, roles)
|> Repo.update()
end
defp clean_roles(nil), do: []
defp clean_roles(roles), do: Enum.filter(roles, &"" != &1)
def update_spoiler_type(%User{} = user, attrs) do
user
|> User.spoiler_type_changeset(attrs)

View file

@ -36,7 +36,7 @@ defmodule Philomena.Users.User do
has_many :notifications, through: [:unread_notifications, :notification]
has_many :linked_tags, through: [:verified_links, :tag]
has_one :commission, Commission
many_to_many :roles, Role, join_through: "users_roles"
many_to_many :roles, Role, join_through: "users_roles", on_replace: :delete
belongs_to :current_filter, Filter
belongs_to :deleted_by_user, User
@ -147,6 +147,15 @@ defmodule Philomena.Users.User do
|> unique_constraint(:email, name: :index_users_on_email)
end
def update_changeset(user, attrs, roles) do
user
|> cast(attrs, [:name, :email, :role, :secondary_role, :hide_default_role])
|> validate_required([:name, :email, :role])
|> validate_inclusion(:role, ["user", "assistant", "moderator", "admin"])
|> put_assoc(:roles, roles)
|> put_slug()
end
def creation_changeset(user, attrs) do
user
|> pow_changeset(attrs)
@ -377,4 +386,4 @@ defmodule Philomena.Users.User do
defp remove_backup_code(user, token),
do: user.otp_backup_codes |> Enum.reject(&Password.verify_pass(token, &1))
end
end

View file

@ -0,0 +1,24 @@
defmodule PhilomenaWeb.Admin.User.AvatarController do
use PhilomenaWeb, :controller
alias Philomena.Users.User
alias Philomena.Users
plug :verify_authorized
plug :load_resource, model: User, id_name: "user_id", id_field: "slug", persisted: true
def delete(conn, _params) do
{:ok, _user} = Users.remove_avatar(conn.assigns.user)
conn
|> put_flash(:info, "Successfully removed avatar.")
|> redirect(to: Routes.admin_user_path(conn, :edit, conn.assigns.user))
end
defp verify_authorized(conn, _opts) do
case Canada.Can.can?(conn.assigns.current_user, :index, User) do
true -> conn
_false -> PhilomenaWeb.NotAuthorizedPlug.call(conn)
end
end
end

View file

@ -0,0 +1,72 @@
defmodule PhilomenaWeb.Admin.UserController do
use PhilomenaWeb, :controller
alias Philomena.Roles.Role
alias Philomena.Users.User
alias Philomena.Users
alias Philomena.Repo
import Ecto.Query
plug :verify_authorized
plug :load_resource, model: User, only: [:edit, :update], id_field: "slug", preload: [:roles]
plug :load_roles when action in [:edit]
def index(conn, %{"q" => q}) do
User
|> where([u], u.email == ^q or ilike(u.name, ^"%#{q}%"))
|> load_users(conn)
end
def index(conn, %{"twofactor" => _twofactor}) do
User
|> where([u], u.otp_required_for_login == true)
|> load_users(conn)
end
def index(conn, %{"staff" => _staff}) do
User
|> where([u], u.role != "user")
|> load_users(conn)
end
def index(conn, _params) do
load_users(User, conn)
end
defp load_users(queryable, conn) do
users =
queryable
|> order_by(desc: :id)
|> Repo.paginate(conn.assigns.scrivener)
render(conn, "index.html", layout_class: "layout--medium", users: users)
end
def edit(conn, _params) do
changeset = Users.change_user(conn.assigns.user)
render(conn, "edit.html", changeset: changeset)
end
def update(conn, %{"user" => user_params}) do
case Users.update_user(conn.assigns.user, user_params) do
{:ok, _user} ->
conn
|> put_flash(:info, "User successfully updated.")
|> redirect(to: Routes.admin_user_path(conn, :index))
{:error, changeset} ->
render(conn, "edit.html", changeset: changeset)
end
end
defp verify_authorized(conn, _opts) do
case Canada.Can.can?(conn.assigns.current_user, :index, User) do
true -> conn
_false -> PhilomenaWeb.NotAuthorizedPlug.call(conn)
end
end
defp load_roles(conn, _opts) do
assign(conn, :roles, Repo.all(Role))
end
end

View file

@ -206,6 +206,9 @@ defmodule PhilomenaWeb.Router do
resources "/forums", ForumController, except: [:show, :delete]
resources "/badges", BadgeController, except: [:show, :delete]
resources "/mod_notes", ModNoteController, except: [:show]
resources "/users", UserController, only: [:index, :edit, :update] do
resources "/avatar", User.AvatarController, only: [:delete], singleton: true
end
end
resources "/duplicate_reports", DuplicateReportController, only: [] do

View file

@ -0,0 +1,44 @@
= form_for @changeset, @action, fn f ->
= if @changeset.action do
.alert.alert-danger
p Oops, something went wrong! Please check the errors below.
.block
.block__header
span.block__header__title Essential user details
label.table-list__label
.table-list__label__text Name:
.table-list__label__input = text_input f, :name, class: "input"
label.table-list__label
.table-list__label__text Email:
.table-list__label__input = text_input f, :email, class: "input"
label.table-list__label
.table-list__label__text Role:
.table-list__label__input = select f, :role, ["user", "assistant", "moderator", "admin"], class: "input"
label.table-list__label
.table-list__label__text Secondary banner:
.table-list__label__input = select f, :secondary_role, [[key: "-", value: ""], "Site Developer", "System Administrator"], class: "input"
label.table-list__label
.table-list__label__text Hide staff banner:
.table-list__label__input = checkbox f, :hide_default_role, class: "checkbox"
.table-list__label
.table-list__label__text Avatar
.table-list__label__input
= link "Remove avatar", to: Routes.admin_user_avatar_path(@conn, :delete, @user), class: "button", data: [method: "delete", confirm: "Are you really, really sure?"]
.block
.block__header
span.block__header__title General user flags
ul = collection_checkboxes f, :roles, filtered_roles(general_permissions, @roles), mapper: &checkbox_mapper/6
.block
.block__header.warning
span.block__header__title Special roles for assistants
ul = collection_checkboxes f, :roles, filtered_roles(assistant_permissions, @roles), mapper: &checkbox_mapper/6
.block
.block__header.danger
span.block__header__title Special roles for moderators
ul = collection_checkboxes f, :roles, filtered_roles(moderator_permissions, @roles), mapper: &checkbox_mapper/6
= submit "Save User", class: "button"

View file

@ -0,0 +1,3 @@
h1 Editing user
= render PhilomenaWeb.Admin.UserView, "_form.html", Map.put(assigns, :action, Routes.admin_user_path(@conn, :update, @user))

View file

@ -0,0 +1,75 @@
h1 Users
= form_for :user, Routes.admin_user_path(@conn, :index), [method: "get", class: "hform"], fn f ->
.field
=> text_input f, :q, name: "q", class: "hform__text input", placeholder: "Search query"
= submit "Search", class: "button hform__button"
=> link "Site staff", to: Routes.admin_user_path(@conn, :index, staff: 1)
' •
=> link "2FA users", to: Routes.admin_user_path(@conn, :index, twofactor: 1)
- route = fn p -> Routes.admin_user_path(@conn, :index, p) end
- pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @users, route: route, conn: @conn
.block
.block__header
= pagination
.block__content
table.table
thead
tr
th Name
th Email
th Activated
th Role
th Created
th Options
tbody
= for user <- @users do
tr
td
= link user.name, to: Routes.profile_path(@conn, :show, user)
= cond do
- user.otp_required_for_login ->
span.banner__2fa.success 2FA
- user.role != "user" and !user.otp_required_for_login ->
span.banner__2fa.danger 1FA
- true ->
td
= user.email
td
= if user.deleted_at do
strong> Deactivated
= pretty_time user.deleted_at
- else
' Active
td
= String.capitalize(user.role)
td
= pretty_time user.created_at
td
=> link "Edit", to: Routes.admin_user_path(@conn, :edit, user)
' &bull;
/= if user.deleted_at do
/ => link_to 'Reactivate', admin_user_activation_path(user), data: { confirm: t('are_you_sure') }, method: :create
/- else
/ => link_to 'Deactivate', admin_user_activation_path(user), data: { confirm: t('are_you_sure') }, method: :delete
/' &bull;
=> link "Ban", to: Routes.admin_user_ban_path(@conn, :new, username: user.name)
' &bull;
=> link "Add link", to: Routes.profile_user_link_path(@conn, :new, user)
.block__header.block__header--light
= pagination

View file

@ -12,7 +12,7 @@
' Site Notices
= if manages_users?(@conn) do
= link to: "#", class: "header__link" do
= link to: Routes.admin_user_path(@conn, :index), class: "header__link" do
i.fa.fa-fw.fa-users>
' Users

View file

@ -1,4 +1,4 @@
h1 Your Links
h1 User Links
p
a.button href=Routes.profile_user_link_path(@conn, :new, @user)
' Create a link

View file

@ -1,2 +1,2 @@
h1 Create Link
= render PhilomenaWeb.UserLinkView, "_form.html", changeset: @changeset, action: Routes.profile_user_link_path(@conn, :create, @user), conn: @conn
= render PhilomenaWeb.Profile.UserLinkView, "_form.html", changeset: @changeset, action: Routes.profile_user_link_path(@conn, :create, @user), conn: @conn

View file

@ -0,0 +1,76 @@
defmodule PhilomenaWeb.Admin.UserView do
use PhilomenaWeb, :view
def checkbox_mapper(form, field, input_opts, role, label_opts, _opts) do
input_id = "user_roles_#{role.id}"
label_opts = [for: input_id]
input_opts =
Keyword.merge(input_opts, [
class: "checkbox", id: input_id, checked_value: to_string(role.id), hidden_input: false,
checked: Enum.member?(Enum.map(input_value(form, field), & &1.id), role.id)
])
content_tag(:li, class: "table-list__label") do
content_tag(:div) do
[
checkbox(form, field, input_opts),
" ",
content_tag(:label, description(role.name, role.resource_type), label_opts),
]
end
end
end
def description("moderator", "Image"), do: "Manage images"
def description("moderator", "DuplicateReport"), do: "Manage duplicates"
def description("moderator", "Comment"), do: "Manage comments"
def description("moderator", "Tag"), do: "Manage tag details"
def description("moderator", "UserLink"), do: "Manage user links"
def description("moderator", "Topic"), do: "Moderate forums"
def description("admin", "Tag"), do: "Alias tags"
def description("batch_update", "Tag"), do: "Update tags in batches"
def description("moderator", "Tag"), do: "Manage tags"
def description("moderator", "User"), do: "Manage users and wipe votes"
def description("admin", "Role"), do: "Manage permissions"
def description("admin", "SiteNotice"), do: "Manage site notices"
def description("admin", "Badge"), do: "Manage badges"
def description("admin", "Advert"), do: "Manage ads"
def description(_name, _resource_type), do: "(unknown permission)"
def filtered_roles(permission_set, roles) do
roles
|> Enum.filter(&Enum.member?(permission_set, [&1.name, &1.resource_type]))
|> Enum.map(&{&1, ""})
end
def general_permissions do
[
["batch_update", "Tag"]
]
end
def assistant_permissions do
[
["moderator", "Image"],
["moderator", "DuplicateReport"],
["moderator", "Comment"],
["moderator", "Tag"],
["moderator", "UserLink"],
["moderator", "Topic"]
]
end
def moderator_permissions do
[
["moderator", "User"],
["admin", "Tag"],
["admin", "Role"],
["admin", "SiteNotice"],
["admin", "Badge"],
["admin", "Advert"]
]
end
end

View file

@ -10,7 +10,7 @@
# We recommend using the bang functions (`insert!`, `update!`
# and so on) as they will fail if something goes wrong.
alias Philomena.{Repo, Comments.Comment, Filters.Filter, Forums.Forum, Galleries.Gallery, Posts.Post, Images.Image, Reports.Report, Tags.Tag, Users.User}
alias Philomena.{Repo, Comments.Comment, Filters.Filter, Forums.Forum, Galleries.Gallery, Posts.Post, Images.Image, Reports.Report, Roles.Role, Tags.Tag, Users.User}
alias Philomena.Tags
import Ecto.Query
@ -61,7 +61,14 @@ for user_def <- resources["users"] do
|> Repo.insert(on_conflict: :nothing)
end
IO.puts "---- Generating roles"
for role_def <- resources["roles"] do
%Role{name: role_def["name"], resource_type: role_def["resource_type"]}
|> Role.changeset(%{})
|> Repo.insert(on_conflict: :nothing)
end
IO.puts "---- Indexing content"
Tag.reindex(Tag |> preload(^Tags.indexing_preloads()))
IO.puts "---- Done."
IO.puts "---- Done."

View file

@ -75,5 +75,20 @@
"semi-grimdark",
"grimdark",
"grotesque"
]
}
],
"roles": [
{"name": "moderator", "resource_type": "Image"},
{"name": "moderator", "resource_type": "DuplicateReport"},
{"name": "moderator", "resource_type": "Comment"},
{"name": "moderator", "resource_type": "Tag"},
{"name": "moderator", "resource_type": "UserLink"},
{"name": "admin", "resource_type": "Tag"},
{"name": "moderator", "resource_type": "User"},
{"name": "admin", "resource_type": "SiteNotice"},
{"name": "admin", "resource_type": "Badge"},
{"name": "admin", "resource_type": "Role"},
{"name": "batch_update", "resource_type": "Tag"},
{"name": "moderator", "resource_type": "Topic"},
{"name": "admin", "resource_type": "Advert"}
]
}