mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-30 14:57:59 +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.Uploader
|
||||||
alias Philomena.Users.User
|
alias Philomena.Users.User
|
||||||
|
alias Philomena.Roles.Role
|
||||||
|
|
||||||
use Pow.Ecto.Context,
|
use Pow.Ecto.Context,
|
||||||
repo: Repo,
|
repo: Repo,
|
||||||
|
@ -56,6 +57,7 @@ defmodule Philomena.Users do
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def create_user(attrs \\ %{}) do
|
def create_user(attrs \\ %{}) do
|
||||||
|
roles =
|
||||||
%User{}
|
%User{}
|
||||||
|> User.changeset(attrs)
|
|> User.changeset(attrs)
|
||||||
|> Repo.insert()
|
|> Repo.insert()
|
||||||
|
@ -74,11 +76,19 @@ defmodule Philomena.Users do
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def update_user(%User{} = user, attrs) do
|
def update_user(%User{} = user, attrs) do
|
||||||
|
roles =
|
||||||
|
Role
|
||||||
|
|> where([r], r.id in ^clean_roles(attrs["roles"]))
|
||||||
|
|> Repo.all()
|
||||||
|
|
||||||
user
|
user
|
||||||
|> User.changeset(attrs)
|
|> User.update_changeset(attrs, roles)
|
||||||
|> Repo.update()
|
|> Repo.update()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp clean_roles(nil), do: []
|
||||||
|
defp clean_roles(roles), do: Enum.filter(roles, &"" != &1)
|
||||||
|
|
||||||
def update_spoiler_type(%User{} = user, attrs) do
|
def update_spoiler_type(%User{} = user, attrs) do
|
||||||
user
|
user
|
||||||
|> User.spoiler_type_changeset(attrs)
|
|> User.spoiler_type_changeset(attrs)
|
||||||
|
|
|
@ -36,7 +36,7 @@ defmodule Philomena.Users.User do
|
||||||
has_many :notifications, through: [:unread_notifications, :notification]
|
has_many :notifications, through: [:unread_notifications, :notification]
|
||||||
has_many :linked_tags, through: [:verified_links, :tag]
|
has_many :linked_tags, through: [:verified_links, :tag]
|
||||||
has_one :commission, Commission
|
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 :current_filter, Filter
|
||||||
belongs_to :deleted_by_user, User
|
belongs_to :deleted_by_user, User
|
||||||
|
@ -147,6 +147,15 @@ defmodule Philomena.Users.User do
|
||||||
|> unique_constraint(:email, name: :index_users_on_email)
|
|> unique_constraint(:email, name: :index_users_on_email)
|
||||||
end
|
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
|
def creation_changeset(user, attrs) do
|
||||||
user
|
user
|
||||||
|> pow_changeset(attrs)
|
|> 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 "/forums", ForumController, except: [:show, :delete]
|
||||||
resources "/badges", BadgeController, except: [:show, :delete]
|
resources "/badges", BadgeController, except: [:show, :delete]
|
||||||
resources "/mod_notes", ModNoteController, except: [:show]
|
resources "/mod_notes", ModNoteController, except: [:show]
|
||||||
|
resources "/users", UserController, only: [:index, :edit, :update] do
|
||||||
|
resources "/avatar", User.AvatarController, only: [:delete], singleton: true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
resources "/duplicate_reports", DuplicateReportController, only: [] do
|
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
|
' Site Notices
|
||||||
|
|
||||||
= if manages_users?(@conn) do
|
= 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>
|
i.fa.fa-fw.fa-users>
|
||||||
' Users
|
' Users
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
h1 Your Links
|
h1 User Links
|
||||||
p
|
p
|
||||||
a.button href=Routes.profile_user_link_path(@conn, :new, @user)
|
a.button href=Routes.profile_user_link_path(@conn, :new, @user)
|
||||||
' Create a link
|
' Create a link
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
h1 Create Link
|
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!`
|
# We recommend using the bang functions (`insert!`, `update!`
|
||||||
# and so on) as they will fail if something goes wrong.
|
# 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
|
alias Philomena.Tags
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
|
@ -61,6 +61,13 @@ for user_def <- resources["users"] do
|
||||||
|> Repo.insert(on_conflict: :nothing)
|
|> Repo.insert(on_conflict: :nothing)
|
||||||
end
|
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"
|
IO.puts "---- Indexing content"
|
||||||
Tag.reindex(Tag |> preload(^Tags.indexing_preloads()))
|
Tag.reindex(Tag |> preload(^Tags.indexing_preloads()))
|
||||||
|
|
||||||
|
|
|
@ -75,5 +75,20 @@
|
||||||
"semi-grimdark",
|
"semi-grimdark",
|
||||||
"grimdark",
|
"grimdark",
|
||||||
"grotesque"
|
"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