diff --git a/lib/philomena/comments/comment.ex b/lib/philomena/comments/comment.ex index 180aaaaa..079bdca1 100644 --- a/lib/philomena/comments/comment.ex +++ b/lib/philomena/comments/comment.ex @@ -22,6 +22,7 @@ defmodule Philomena.Comments.Comment do field :deletion_reason, :string, default: "" field :destroyed_content, :boolean, default: false field :name_at_post_time, :string + field :approved_at, :utc_datetime timestamps(inserted_at: :created_at, type: :utc_datetime) end diff --git a/lib/philomena/images/image.ex b/lib/philomena/images/image.ex index 908301c4..91a1ac09 100644 --- a/lib/philomena/images/image.ex +++ b/lib/philomena/images/image.ex @@ -82,6 +82,7 @@ defmodule Philomena.Images.Image do field :hidden_image_key, :string field :scratchpad, :string field :hides_count, :integer, default: 0 + field :approved_at, :utc_datetime # todo: can probably remove these now field :tag_list_cache, :string diff --git a/lib/philomena/posts/post.ex b/lib/philomena/posts/post.ex index b3c3c42f..68e35d7c 100644 --- a/lib/philomena/posts/post.ex +++ b/lib/philomena/posts/post.ex @@ -23,6 +23,7 @@ defmodule Philomena.Posts.Post do field :deletion_reason, :string, default: "" field :destroyed_content, :boolean, default: false field :name_at_post_time, :string + field :approved_at, :utc_datetime timestamps(inserted_at: :created_at, type: :utc_datetime) end diff --git a/lib/philomena/reports/report.ex b/lib/philomena/reports/report.ex index fae713cd..461b6eea 100644 --- a/lib/philomena/reports/report.ex +++ b/lib/philomena/reports/report.ex @@ -15,6 +15,7 @@ defmodule Philomena.Reports.Report do field :reason, :string field :state, :string, default: "open" field :open, :boolean, default: true + field :system, :boolean, default: false # fixme: rails polymorphic relation field :reportable_id, :integer diff --git a/lib/philomena/topics/topic.ex b/lib/philomena/topics/topic.ex index efc49ce4..886a055a 100644 --- a/lib/philomena/topics/topic.ex +++ b/lib/philomena/topics/topic.ex @@ -31,6 +31,7 @@ defmodule Philomena.Topics.Topic do field :slug, :string field :anonymous, :boolean, default: false field :hidden_from_users, :boolean, default: false + field :approved_at, :utc_datetime timestamps(inserted_at: :created_at, type: :utc_datetime) end diff --git a/lib/philomena/users.ex b/lib/philomena/users.ex index 252d67e7..0ce7a825 100644 --- a/lib/philomena/users.ex +++ b/lib/philomena/users.ex @@ -671,6 +671,18 @@ defmodule Philomena.Users do |> setup_roles() end + def verify_user(%User{} = user) do + user + |> User.verify_changeset() + |> Repo.update() + end + + def unverify_user(%User{} = user) do + user + |> User.unverify_changeset() + |> Repo.update() + end + defp setup_roles(nil), do: nil defp setup_roles(user) do diff --git a/lib/philomena/users/user.ex b/lib/philomena/users/user.ex index 3a4ee253..1972241d 100644 --- a/lib/philomena/users/user.ex +++ b/lib/philomena/users/user.ex @@ -119,6 +119,7 @@ defmodule Philomena.Users.User do field :hide_default_role, :boolean, default: false field :senior_staff, :boolean, default: false field :bypass_rate_limits, :boolean, default: false + field :verified, :boolean, default: false # For avatar validation/persistence field :avatar_width, :integer, virtual: true @@ -446,6 +447,14 @@ defmodule Philomena.Users.User do change(user, forced_filter_id: nil) end + def verify_changeset(user) do + change(user, verified: true) + end + + def unverify_changeset(user) do + change(user, verified: false) + end + def create_totp_secret_changeset(user) do secret = :crypto.strong_rand_bytes(15) |> Base.encode32() data = Philomena.Users.Encryptor.encrypt_model(secret) diff --git a/lib/philomena_web/controllers/admin/user/verification_controller.ex b/lib/philomena_web/controllers/admin/user/verification_controller.ex new file mode 100644 index 00000000..982a520c --- /dev/null +++ b/lib/philomena_web/controllers/admin/user/verification_controller.ex @@ -0,0 +1,44 @@ +defmodule PhilomenaWeb.Admin.User.VerificationController 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 create(conn, _params) do + {:ok, user} = Users.verify_user(conn.assigns.user) + + conn + |> put_flash(:info, "User verification granted.") + |> moderation_log(details: &log_details/3, data: user) + |> redirect(to: Routes.profile_path(conn, :show, user)) + end + + def delete(conn, _params) do + {:ok, user} = Users.unverify_user(conn.assigns.user) + + conn + |> put_flash(:info, "User verification revoked.") + |> moderation_log(details: &log_details/3, data: user) + |> redirect(to: Routes.profile_path(conn, :show, 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 + + defp log_details(conn, action, user) do + body = + case action do + :create -> "Granted verification to #{user.name}" + :delete -> "Revoked verification from #{user.name}" + end + + %{body: body, subject_path: Routes.profile_path(conn, :show, user)} + end +end diff --git a/lib/philomena_web/router.ex b/lib/philomena_web/router.ex index ad7f6a24..9b4d902c 100644 --- a/lib/philomena_web/router.ex +++ b/lib/philomena_web/router.ex @@ -379,6 +379,8 @@ defmodule PhilomenaWeb.Router do only: [:create, :delete], singleton: true + resources "/verification", User.VerificationController, only: [:create, :delete], singleton: true + resources "/unlock", User.UnlockController, only: [:create], singleton: true resources "/api_key", User.ApiKeyController, only: [:delete], singleton: true resources "/downvotes", User.DownvoteController, only: [:delete], singleton: true diff --git a/lib/philomena_web/templates/profile/_admin_block.html.slime b/lib/philomena_web/templates/profile/_admin_block.html.slime index 81ddaf04..fdb3bd53 100644 --- a/lib/philomena_web/templates/profile/_admin_block.html.slime +++ b/lib/philomena_web/templates/profile/_admin_block.html.slime @@ -153,8 +153,19 @@ a.label.label--primary.label--block href="#" data-click-toggle=".js-admin__optio i.fa.fa-fw.fa-ban span.admin__button Ban this sucker + ul.profile-admin__options__column = if can?(@conn, :index, Philomena.Users.User) do li = link to: Routes.admin_user_api_key_path(@conn, :delete, @user), data: [confirm: "Are you really, really sure?", method: "delete"] do i.fas.fa-fw.fa-key span.admin__button Reset API key + + li + = if @user.verified do + = link to: Routes.admin_user_verification_path(@conn, :delete, @user), data: [confirm: "Are you really, really sure?", method: "delete"] do + i.fas.fa-fw.fa-user-times + span.admin__button Revoke Verification + - else + = link to: Routes.admin_user_verification_path(@conn, :create, @user), data: [confirm: "Are you really, really sure?", method: "create"] do + i.fas.fa-fw.fa-user-check + span.admin__button Grant Verification diff --git a/priv/repo/migrations/20220321173359_add_approval_queue.exs b/priv/repo/migrations/20220321173359_add_approval_queue.exs new file mode 100644 index 00000000..a03f616d --- /dev/null +++ b/priv/repo/migrations/20220321173359_add_approval_queue.exs @@ -0,0 +1,29 @@ +defmodule Philomena.Repo.Migrations.AddApprovalQueue do + use Ecto.Migration + + def change do + alter table("reports") do + add :system, :boolean, default: false + end + + alter table("images") do + add :approved_at, :utc_datetime + end + + alter table("comments") do + add :approved_at, :utc_datetime + end + + alter table("posts") do + add :approved_at, :utc_datetime + end + + alter table("topics") do + add :approved_at, :utc_datetime + end + + alter table("users") do + add :verified, :boolean, default: false + end + end +end diff --git a/priv/repo/structure.sql b/priv/repo/structure.sql index 0a05672e..1f64996c 100644 --- a/priv/repo/structure.sql +++ b/priv/repo/structure.sql @@ -282,7 +282,8 @@ CREATE TABLE public.comments ( deletion_reason character varying DEFAULT ''::character varying NOT NULL, destroyed_content boolean DEFAULT false, name_at_post_time character varying, - body character varying NOT NULL + body character varying NOT NULL, + approved_at timestamp(0) without time zone ); @@ -971,7 +972,8 @@ CREATE TABLE public.images ( hides_count integer DEFAULT 0 NOT NULL, image_duration double precision, description character varying DEFAULT ''::character varying NOT NULL, - scratchpad character varying + scratchpad character varying, + approved_at timestamp(0) without time zone ); @@ -1258,7 +1260,8 @@ CREATE TABLE public.posts ( deletion_reason character varying DEFAULT ''::character varying NOT NULL, destroyed_content boolean DEFAULT false NOT NULL, name_at_post_time character varying, - body character varying NOT NULL + body character varying NOT NULL, + approved_at timestamp(0) without time zone ); @@ -1300,7 +1303,8 @@ CREATE TABLE public.reports ( admin_id integer, reportable_id integer NOT NULL, reportable_type character varying NOT NULL, - reason character varying NOT NULL + reason character varying NOT NULL, + system boolean DEFAULT false ); @@ -1676,7 +1680,8 @@ CREATE TABLE public.topics ( deleted_by_id integer, locked_by_id integer, last_post_id integer, - hidden_from_users boolean DEFAULT false NOT NULL + hidden_from_users boolean DEFAULT false NOT NULL, + approved_at timestamp(0) without time zone ); @@ -2050,7 +2055,8 @@ CREATE TABLE public.users ( description character varying, scratchpad character varying, bypass_rate_limits boolean DEFAULT false, - scale_large_images character varying(255) DEFAULT 'true'::character varying NOT NULL + scale_large_images character varying(255) DEFAULT 'true'::character varying NOT NULL, + verified boolean DEFAULT false ); @@ -4970,3 +4976,4 @@ INSERT INTO public."schema_migrations" (version) VALUES (20210921025336); INSERT INTO public."schema_migrations" (version) VALUES (20210929181319); INSERT INTO public."schema_migrations" (version) VALUES (20211107130226); INSERT INTO public."schema_migrations" (version) VALUES (20211219194836); +INSERT INTO public."schema_migrations" (version) VALUES (20220321173359);