mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-27 13:47:58 +01:00
admin users and roles
This commit is contained in:
parent
f5996dc084
commit
f8431cb1c2
14 changed files with 348 additions and 10 deletions
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
72
lib/philomena_web/controllers/admin/user_controller.ex
Normal file
72
lib/philomena_web/controllers/admin/user_controller.ex
Normal 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
|
|
@ -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
|
||||
|
|
44
lib/philomena_web/templates/admin/user/_form.html.slime
Normal file
44
lib/philomena_web/templates/admin/user/_form.html.slime
Normal 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"
|
3
lib/philomena_web/templates/admin/user/edit.html.slime
Normal file
3
lib/philomena_web/templates/admin/user/edit.html.slime
Normal 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))
|
75
lib/philomena_web/templates/admin/user/index.html.slime
Normal file
75
lib/philomena_web/templates/admin/user/index.html.slime
Normal 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)
|
||||
' •
|
||||
|
||||
/= 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
|
||||
/' •
|
||||
|
||||
=> link "Ban", to: Routes.admin_user_ban_path(@conn, :new, username: user.name)
|
||||
' •
|
||||
=> link "Add link", to: Routes.profile_user_link_path(@conn, :new, user)
|
||||
|
||||
.block__header.block__header--light
|
||||
= pagination
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
76
lib/philomena_web/views/admin/user_view.ex
Normal file
76
lib/philomena_web/views/admin/user_view.ex
Normal 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
|
|
@ -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,6 +61,13 @@ 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()))
|
||||
|
||||
|
|
|
@ -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"}
|
||||
]
|
||||
}
|
Loading…
Reference in a new issue