From 1988af5406b2940f87d94578e7251d115d288338 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 7 Oct 2024 15:19:00 -1000 Subject: [PATCH 01/12] refactor(user-find): wip implement user find route --- lib/epochtalk_server_web/controllers/user.ex | 23 +++++++++++++++ lib/epochtalk_server_web/json/user_json.ex | 31 ++++++++++++++++++++ lib/epochtalk_server_web/router.ex | 1 + 3 files changed, 55 insertions(+) diff --git a/lib/epochtalk_server_web/controllers/user.ex b/lib/epochtalk_server_web/controllers/user.ex index 929b4693..d14180fb 100644 --- a/lib/epochtalk_server_web/controllers/user.ex +++ b/lib/epochtalk_server_web/controllers/user.ex @@ -5,6 +5,7 @@ defmodule EpochtalkServerWeb.Controllers.User do Controller For `User` related API requests """ alias EpochtalkServer.Models.User + alias EpochtalkServer.Models.UserActivity alias EpochtalkServer.Models.Ban alias EpochtalkServer.Models.Invitation alias EpochtalkServer.Auth.Guardian @@ -146,6 +147,28 @@ defmodule EpochtalkServerWeb.Controllers.User do def confirm(_conn, _attrs), do: raise(InvalidPayload) + @doc """ + Finds a `User`. + """ + def find(conn, %{"username" => username}) do + # create user + with {:ok, user} <- User.by_username(username), + activity <- UserActivity.get_by_user_id(user.id) do + render(conn, :find, %{user: user, activity: activity}) + else + {:error, :user_not_found} -> + ErrorHelpers.render_json_error(conn, 400, "Account not found") + + {:error, data} -> + ErrorHelpers.render_json_error(conn, 400, data) + + _ -> + ErrorHelpers.render_json_error(conn, 500, "There was an issue finding user") + end + end + + def find(_conn, _attrs), do: raise(InvalidPayload) + @doc """ Authenticates currently logged in `User` """ diff --git a/lib/epochtalk_server_web/json/user_json.ex b/lib/epochtalk_server_web/json/user_json.ex index 4f10398b..432bb43c 100644 --- a/lib/epochtalk_server_web/json/user_json.ex +++ b/lib/epochtalk_server_web/json/user_json.ex @@ -13,6 +13,37 @@ defmodule EpochtalkServerWeb.Controllers.UserJSON do """ def user(%{user: user, token: token}), do: format_user_reply(user, token) + @doc """ + Renders formatted user JSON for find + """ + def find(%{user: user, activity: activity}) do + highest_role = user.roles |> Enum.sort_by(&(&1.priority)) |> List.first() + %{ + id: user.id, + username: user.username, + email: user.email, + created_at: user.created_at, + updated_at: user.updated_at, + avatar: user.profile.avatar, + post_count: user.profile.post_count, + last_active: user.profile.last_active, + threads_per_page: user.preferences.threads_per_page, + posts_per_page: user.preferences.posts_per_page, + dob: (if user.profile.fields, do: user.profile.fields[:dob]), + name: (if user.profile.fields, do: user.profile.fields[:name]), + gender: (if user.profile.fields, do: user.profile.fields[:gender]), + website: (if user.profile.fields, do: user.profile.fields[:website]), + language: (if user.profile.fields, do: user.profile.fields[:language]), + location: (if user.profile.fields, do: user.profile.fields[:location]), + role_name: Map.get(highest_role, :name), + role_highlight_color: Map.get(highest_role, :highlight_color), + roles: Enum.map(user.roles, &(&1.lookup)), + priority: Map.get(highest_role, :priority), + metadata: %{}, # TODO(akinsey): implement metric rank map db functions + activity: activity + } + end + @doc """ Renders formatted JSON response for registration confirmation. ## Example diff --git a/lib/epochtalk_server_web/router.ex b/lib/epochtalk_server_web/router.ex index 79945658..98a639ef 100644 --- a/lib/epochtalk_server_web/router.ex +++ b/lib/epochtalk_server_web/router.ex @@ -73,6 +73,7 @@ defmodule EpochtalkServerWeb.Router do get "/boards/:id", Board, :find get "/boards/:slug/id", Board, :slug_to_id get "/breadcrumbs", Breadcrumb, :breadcrumbs + get "/users/:username", User, :find get "/posts", Post, :by_thread get "/threads", Thread, :by_board get "/threads/:slug/id", Thread, :slug_to_id From 863510dfb1f713a31f382a2e6744333bcfb7a505 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 8 Oct 2024 14:07:05 -1000 Subject: [PATCH 02/12] refactor(user-find): add rank and metric maps to user find query --- lib/epochtalk_server_web/controllers/user.ex | 13 +++++++-- lib/epochtalk_server_web/json/user_json.ex | 30 +++++++++++--------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/lib/epochtalk_server_web/controllers/user.ex b/lib/epochtalk_server_web/controllers/user.ex index d14180fb..5bb844c4 100644 --- a/lib/epochtalk_server_web/controllers/user.ex +++ b/lib/epochtalk_server_web/controllers/user.ex @@ -7,6 +7,8 @@ defmodule EpochtalkServerWeb.Controllers.User do alias EpochtalkServer.Models.User alias EpochtalkServer.Models.UserActivity alias EpochtalkServer.Models.Ban + alias EpochtalkServer.Models.MetricRankMap + alias EpochtalkServer.Models.Rank alias EpochtalkServer.Models.Invitation alias EpochtalkServer.Auth.Guardian alias EpochtalkServer.Session @@ -153,8 +155,15 @@ defmodule EpochtalkServerWeb.Controllers.User do def find(conn, %{"username" => username}) do # create user with {:ok, user} <- User.by_username(username), - activity <- UserActivity.get_by_user_id(user.id) do - render(conn, :find, %{user: user, activity: activity}) + activity <- UserActivity.get_by_user_id(user.id), + metric_rank_maps <- MetricRankMap.all_merged(), + ranks <- Rank.all() do + render(conn, :find, %{ + user: user, + activity: activity, + metric_rank_maps: metric_rank_maps, + ranks: ranks + }) else {:error, :user_not_found} -> ErrorHelpers.render_json_error(conn, 400, "Account not found") diff --git a/lib/epochtalk_server_web/json/user_json.ex b/lib/epochtalk_server_web/json/user_json.ex index 432bb43c..bce95389 100644 --- a/lib/epochtalk_server_web/json/user_json.ex +++ b/lib/epochtalk_server_web/json/user_json.ex @@ -16,11 +16,12 @@ defmodule EpochtalkServerWeb.Controllers.UserJSON do @doc """ Renders formatted user JSON for find """ - def find(%{user: user, activity: activity}) do - highest_role = user.roles |> Enum.sort_by(&(&1.priority)) |> List.first() + def find(%{user: user, activity: activity, metric_rank_maps: metric_rank_maps, ranks: ranks}) do + highest_role = user.roles |> Enum.sort_by(& &1.priority) |> List.first() + %{ id: user.id, - username: user.username, + username: user.username, email: user.email, created_at: user.created_at, updated_at: user.updated_at, @@ -29,19 +30,22 @@ defmodule EpochtalkServerWeb.Controllers.UserJSON do last_active: user.profile.last_active, threads_per_page: user.preferences.threads_per_page, posts_per_page: user.preferences.posts_per_page, - dob: (if user.profile.fields, do: user.profile.fields[:dob]), - name: (if user.profile.fields, do: user.profile.fields[:name]), - gender: (if user.profile.fields, do: user.profile.fields[:gender]), - website: (if user.profile.fields, do: user.profile.fields[:website]), - language: (if user.profile.fields, do: user.profile.fields[:language]), - location: (if user.profile.fields, do: user.profile.fields[:location]), + dob: if(user.profile.fields, do: user.profile.fields[:dob]), + name: if(user.profile.fields, do: user.profile.fields[:name]), + gender: if(user.profile.fields, do: user.profile.fields[:gender]), + website: if(user.profile.fields, do: user.profile.fields[:website]), + language: if(user.profile.fields, do: user.profile.fields[:language]), + location: if(user.profile.fields, do: user.profile.fields[:location]), role_name: Map.get(highest_role, :name), role_highlight_color: Map.get(highest_role, :highlight_color), - roles: Enum.map(user.roles, &(&1.lookup)), + roles: Enum.map(user.roles, & &1.lookup), priority: Map.get(highest_role, :priority), - metadata: %{}, # TODO(akinsey): implement metric rank map db functions - activity: activity - } + metadata: %{ + rank_metric_maps: metric_rank_maps, + ranks: ranks + }, + activity: activity + } end @doc """ From 0b736a89dbb5b7f48f94992d1cb14d6c755b470e Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 10 Oct 2024 11:46:21 -1000 Subject: [PATCH 03/12] feat(user-find): add authorizations to user find --- lib/epochtalk_server_web/controllers/user.ex | 21 ++++++++- lib/epochtalk_server_web/json/user_json.ex | 47 +++++++++++++------- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/lib/epochtalk_server_web/controllers/user.ex b/lib/epochtalk_server_web/controllers/user.ex index 5bb844c4..f3a11313 100644 --- a/lib/epochtalk_server_web/controllers/user.ex +++ b/lib/epochtalk_server_web/controllers/user.ex @@ -15,6 +15,7 @@ defmodule EpochtalkServerWeb.Controllers.User do alias EpochtalkServer.Mailer alias EpochtalkServerWeb.ErrorHelpers alias EpochtalkServerWeb.CustomErrors.InvalidPayload + alias EpochtalkServerWeb.Helpers.ACL alias EpochtalkServerWeb.Helpers.Validate @doc """ @@ -157,12 +158,25 @@ defmodule EpochtalkServerWeb.Controllers.User do with {:ok, user} <- User.by_username(username), activity <- UserActivity.get_by_user_id(user.id), metric_rank_maps <- MetricRankMap.all_merged(), - ranks <- Rank.all() do + ranks <- Rank.all(), + authed_user <- Guardian.Plug.current_resource(conn), + # Authorizations Checks + :ok <- ACL.allow!(conn, "users.find"), + {:user_not_deleted, user_not_deleted} <- + {:user_not_deleted, (if user.id || !user.deleted, do: true, else: false)}, + {:has_deleted_override, has_deleted_override} <- + {:has_deleted_override, ACL.has_permission(conn, "users.find.bypass.viewDeleted")}, + {:view_deleted, true} <- {:view_deleted, user_not_deleted || has_deleted_override}, + {:view_as_self, view_as_self} <- {:view_as_self, authed_user && authed_user.id == user.id}, + {:view_as_admin, view_as_admin} <- + {:view_as_admin, ACL.has_permission(conn, "users.find.bypass.viewMoreInfo")}, + {:show_hidden, show_hidden} <- {:show_hidden, view_as_self || view_as_admin} do render(conn, :find, %{ user: user, activity: activity, metric_rank_maps: metric_rank_maps, - ranks: ranks + ranks: ranks, + show_hidden: show_hidden }) else {:error, :user_not_found} -> @@ -171,6 +185,9 @@ defmodule EpochtalkServerWeb.Controllers.User do {:error, data} -> ErrorHelpers.render_json_error(conn, 400, data) + {:view_deleted, false} -> + ErrorHelpers.render_json_error(conn, 400, "Account not found") + _ -> ErrorHelpers.render_json_error(conn, 500, "There was an issue finding user") end diff --git a/lib/epochtalk_server_web/json/user_json.ex b/lib/epochtalk_server_web/json/user_json.ex index bce95389..17d6f361 100644 --- a/lib/epochtalk_server_web/json/user_json.ex +++ b/lib/epochtalk_server_web/json/user_json.ex @@ -16,36 +16,53 @@ defmodule EpochtalkServerWeb.Controllers.UserJSON do @doc """ Renders formatted user JSON for find """ - def find(%{user: user, activity: activity, metric_rank_maps: metric_rank_maps, ranks: ranks}) do + def find(%{ + user: user, + activity: activity, + metric_rank_maps: metric_rank_maps, + ranks: ranks, + show_hidden: show_hidden + }) do highest_role = user.roles |> Enum.sort_by(& &1.priority) |> List.first() + [post_count | _] = metric_rank_maps - %{ + # handle missing profile/preferences row + user = user |> Map.put(:profile, user.profile || %{}) + user = user |> Map.put(:preferences, user.preferences || %{}) + + ret = %{ id: user.id, username: user.username, - email: user.email, created_at: user.created_at, updated_at: user.updated_at, - avatar: user.profile.avatar, - post_count: user.profile.post_count, - last_active: user.profile.last_active, - threads_per_page: user.preferences.threads_per_page, - posts_per_page: user.preferences.posts_per_page, - dob: if(user.profile.fields, do: user.profile.fields[:dob]), - name: if(user.profile.fields, do: user.profile.fields[:name]), - gender: if(user.profile.fields, do: user.profile.fields[:gender]), - website: if(user.profile.fields, do: user.profile.fields[:website]), - language: if(user.profile.fields, do: user.profile.fields[:language]), - location: if(user.profile.fields, do: user.profile.fields[:location]), + avatar: Map.get(user.profile, :avatar), + post_count: Map.get(user.profile, :post_count), + last_active: Map.get(user.profile, :last_active), + dob: (if Map.get(user.profile, :fields), do: user.profile.fields["dob"]), + name: (if Map.get(user.profile, :fields), do: user.profile.fields["name"]), + gender: (if Map.get(user.profile, :fields), do: user.profile.fields["gender"]), + website: (if Map.get(user.profile, :fields), do: user.profile.fields["website"]), + language: (if Map.get(user.profile, :fields), do: user.profile.fields["language"]), + location: (if Map.get(user.profile, :fields), do: user.profile.fields["location"]), role_name: Map.get(highest_role, :name), role_highlight_color: Map.get(highest_role, :highlight_color), roles: Enum.map(user.roles, & &1.lookup), priority: Map.get(highest_role, :priority), metadata: %{ - rank_metric_maps: metric_rank_maps, + rank_metric_maps: post_count.maps, ranks: ranks }, activity: activity } + + if show_hidden, + do: ret + |> Map.put(:email, user.email) + |> Map.put(:threads_per_page, Map.get(user.preferences, :threads_per_page) || 25) + |> Map.put(:posts_per_page, Map.get(user.preferences, :posts_per_page) || 25) + |> Map.put(:ignored_boards, Map.get(user.preferences, :ignored_boards) || []) + |> Map.put(:collapsed_categories, Map.get(user.preferences, :collapsed_categories) || []), + else: ret end @doc """ From b6211cf1039a91aac4566a18e0599b6765af8430 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Fri, 11 Oct 2024 13:43:14 -1000 Subject: [PATCH 04/12] feat(page-by-username): implement db query for paging posts by username --- lib/epochtalk_server/models/post.ex | 41 +++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/lib/epochtalk_server/models/post.ex b/lib/epochtalk_server/models/post.ex index f4f29038..074dd892 100644 --- a/lib/epochtalk_server/models/post.ex +++ b/lib/epochtalk_server/models/post.ex @@ -3,6 +3,7 @@ defmodule EpochtalkServer.Models.Post do import Ecto.Changeset import Ecto.Query alias EpochtalkServer.Repo + alias EpochtalkServer.Models.Board alias EpochtalkServer.Models.Post alias EpochtalkServer.Models.Thread alias EpochtalkServer.Models.User @@ -412,6 +413,46 @@ defmodule EpochtalkServer.Models.Post do else: results end + @doc """ + Used to page `Post` by a specific `User` given a `username` + """ + @spec page_by_username(username :: String.t(), + priority :: non_neg_integer, + page :: non_neg_integer | nil, + opts :: list() | nil + ) :: [map()] | [] + def page_by_username(username, priority, page \\ 1, opts \\ []) when is_binary(username) do + per_page = Keyword.get(opts, :per_page, 25) + offset = page * per_page - per_page + desc = Keyword.get(opts, :desc, true) == true + direction = if desc, do: :desc, else: :asc + + Post + |> join(:left, [p], t in Thread, on: t.id == p.thread_id) + |> join(:left, [p], u in User, on: u.id == p.user_id) + |> join(:left, [p, t], b in Board, on: b.id == t.board_id) + |> where([p, t, u], u.username == ^username and p.user_id == u.id) + |> order_by([p], [{^direction, p.id}]) + |> limit(^per_page) + |> offset(^offset) + |> select([p, t, u, b], %{ + id: p.id, + position: p.position, + thread_id: p.thread_id, + thread_slug: t.slug, + thread_title: fragment("SELECT content->>'title' as title FROM posts WHERE thread_id = ? ORDER BY id LIMIT 1", p.thread_id), + user: %{id: p.user_id, deleted: u.deleted}, + body: p.content["body"], + deleted: p.deleted, + created_at: p.created_at, + updated_at: p.updated_at, + imported_at: p.imported_at, + board_id: b.id, + board_visible: fragment("EXISTS(SELECT 1 FROM boards WHERE board_id = ? AND (viewable_by >= ? OR viewable_by IS NULL))", b.id, ^priority) + }) + |> Repo.all() + end + @doc """ Used to correct the text search vector for post after being modified for mentions """ From a0a0ab18fa57294ad9454e0a2ba603f3b7ca3ac0 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 14 Oct 2024 09:56:37 -1000 Subject: [PATCH 05/12] feat(post-by-username): add count db call for post by username route --- lib/epochtalk_server/models/profile.ex | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/epochtalk_server/models/profile.ex b/lib/epochtalk_server/models/profile.ex index 74312aa3..1b65e7a2 100644 --- a/lib/epochtalk_server/models/profile.ex +++ b/lib/epochtalk_server/models/profile.ex @@ -53,6 +53,20 @@ defmodule EpochtalkServer.Models.Profile do ## === Database Functions === + @doc """ + Gets the `post_count` field given a `User` username + """ + @spec post_count_by_username(username :: String.t()) :: non_neg_integer() | nil + def post_count_by_username(username) do + query = from p in Profile, + join: u in User, + on: p.user_id == u.id, + where: u.username == ^username, + select: p.post_count + + Repo.one(query) + end + @doc """ Increments the `post_count` field given a `User` id """ From a2cdf5fa8a5a380f3bd653797fa7188e12d707a1 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 14 Oct 2024 13:41:28 -1000 Subject: [PATCH 06/12] refactor(post-by-username): wip implement post by username --- lib/epochtalk_server_web/controllers/post.ex | 66 ++++++++++++++++++++ lib/epochtalk_server_web/json/post_json.ex | 18 ++++-- lib/epochtalk_server_web/router.ex | 1 + 3 files changed, 81 insertions(+), 4 deletions(-) diff --git a/lib/epochtalk_server_web/controllers/post.ex b/lib/epochtalk_server_web/controllers/post.ex index 4e75fca3..a86fd3be 100644 --- a/lib/epochtalk_server_web/controllers/post.ex +++ b/lib/epochtalk_server_web/controllers/post.ex @@ -358,6 +358,51 @@ defmodule EpochtalkServerWeb.Controllers.Post do end end + @doc """ + Used to retrieve `Posts` for a `User` by username + """ + def by_username(conn, attrs) do + # Parameter Validation + with username <- attrs["username"], + page <- Validate.cast(attrs, "page", :integer, default: 1, min: 1), + limit <- Validate.cast(attrs, "limit", :integer, default: 25, min: 1, max: 100), + desc <- Validate.cast(attrs, "desc", :boolean, default: true), + user <- Guardian.Plug.current_resource(conn), + priority <- ACL.get_user_priority(conn), + [lookup_user] <- User.ids_from_usernames([username]), + + # Authorizations Checks + :ok <- ACL.allow!(conn, "posts.pageByUser"), + {:user_not_deleted, user_not_deleted} <- + {:user_not_deleted, User.is_active?(lookup_user.id)}, + {:has_deleted_override, has_deleted_override} <- + {:has_deleted_override, ACL.has_permission(conn, "posts.pageByUser.bypass.viewDeletedUsers")}, + {:view_deleted_users, true} <- {:view_deleted_users, user_not_deleted || has_deleted_override}, + view_deleted_posts <- can_authed_user_view_deleted_posts_by_username(user), + posts <- + Post.page_by_username(username, priority, page, + per_page: limit, + desc: desc + ), + {:has_posts, true} <- {:has_posts, posts != []} do + render(conn, :by_username, %{ + posts: posts, + user: user, + priority: priority, + view_deleted_posts: view_deleted_posts + }) + else + {:has_posts, false} -> + ErrorHelpers.render_json_error(conn, 404, "Error, requested posts not found") + + {:view_deleted_users, false} -> + ErrorHelpers.render_json_error(conn, 400, "Account not found") + + _ -> + ErrorHelpers.render_json_error(conn, 400, "Error, cannot get posts by username") + end + end + @doc """ Get `Post` preview by running content through parser """ @@ -402,6 +447,27 @@ defmodule EpochtalkServerWeb.Controllers.Post do end end + defp can_authed_user_view_deleted_posts_by_username(nil), do: false + + defp can_authed_user_view_deleted_posts_by_username(user) do + view_all = ACL.has_permission(user, "posts.byUsername.bypass.viewDeletedPosts.admin") + view_some = ACL.has_permission(user, "posts.byUsername.bypass.viewDeletedPosts.mod") + + user_id = Map.get(user, :id) + moderated_boards = BoardModerator.get_user_moderated_boards(user_id) + + cond do + view_all -> + true + + view_some and moderated_boards != [] -> + moderated_boards + + true -> + false + end + end + defp can_authed_user_bypass_thread_lock_on_post_create(user, thread_id), do: ACL.bypass_post_owner( diff --git a/lib/epochtalk_server_web/json/post_json.ex b/lib/epochtalk_server_web/json/post_json.ex index 66f99ad3..cdc11015 100644 --- a/lib/epochtalk_server_web/json/post_json.ex +++ b/lib/epochtalk_server_web/json/post_json.ex @@ -99,6 +99,15 @@ defmodule EpochtalkServerWeb.Controllers.PostJSON do } end + @doc """ + Renders all `Post` for a particular `User`. + """ + def by_username(%{posts: posts, user: user, priority: priority, view_deleted_posts: view_deleted_posts}) do + posts + |> Enum.map(&(Map.put(&1, :body_html, &1.body) |> Map.delete(:body))) + |> handle_deleted_posts(nil, user, priority, view_deleted_posts) + end + ## === Private Helper Functions === defp handle_deleted_posts(posts, thread, user, authed_user_priority, view_deleted_posts) do @@ -145,13 +154,12 @@ defmodule EpochtalkServerWeb.Controllers.PostJSON do viewable_in_board_with_id ) do # check if metadata map exists - metadata_map_exists = !!post.metadata and Map.keys(post.metadata) != [] + metadata_map_exists = !!Map.get(post, :metadata) and Map.keys(post.metadata) != [] # get information about how current post was hidden post_hidden_by_priority = if metadata_map_exists && post.metadata["hidden_by_priority"] != nil, - do: post.metadata["hidden_by_priority"], - else: post.user.priority + do: post.metadata["hidden_by_priority"] post_hidden_by_id = if metadata_map_exists && post.metadata["hidden_by_id"] != nil, @@ -160,7 +168,9 @@ defmodule EpochtalkServerWeb.Controllers.PostJSON do # check if user has priority to view hidden post, # or if the user was the one who hid the post - authed_user_has_priority = authed_user_priority <= post_hidden_by_priority + authed_user_has_priority = if is_nil(post_hidden_by_priority), + do: false, + else: authed_user_priority <= post_hidden_by_priority authed_user_hid_post = post_hidden_by_id == authed_user_id post_is_viewable = diff --git a/lib/epochtalk_server_web/router.ex b/lib/epochtalk_server_web/router.ex index 98a639ef..2991a62e 100644 --- a/lib/epochtalk_server_web/router.ex +++ b/lib/epochtalk_server_web/router.ex @@ -75,6 +75,7 @@ defmodule EpochtalkServerWeb.Router do get "/breadcrumbs", Breadcrumb, :breadcrumbs get "/users/:username", User, :find get "/posts", Post, :by_thread + get "/posts/user/:username", Post, :by_username get "/threads", Thread, :by_board get "/threads/:slug/id", Thread, :slug_to_id post "/threads/:id/viewed", Thread, :viewed From f3ab29d10e1471bad0fc53ce7d61ef17cc3aefa0 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 15 Oct 2024 11:22:10 -1000 Subject: [PATCH 07/12] feat(post-by-username): complete implementation for post by username --- lib/epochtalk_server_web/controllers/post.ex | 8 +++++++- lib/epochtalk_server_web/json/post_json.ex | 12 ++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/epochtalk_server_web/controllers/post.ex b/lib/epochtalk_server_web/controllers/post.ex index a86fd3be..ccc618d7 100644 --- a/lib/epochtalk_server_web/controllers/post.ex +++ b/lib/epochtalk_server_web/controllers/post.ex @@ -10,6 +10,7 @@ defmodule EpochtalkServerWeb.Controllers.Post do alias EpochtalkServerWeb.Helpers.ACL alias EpochtalkServerWeb.Helpers.Sanitize alias EpochtalkServerWeb.Helpers.Parse + alias EpochtalkServer.Models.Profile alias EpochtalkServer.Models.Post alias EpochtalkServer.Models.Poll alias EpochtalkServer.Models.Thread @@ -384,12 +385,17 @@ defmodule EpochtalkServerWeb.Controllers.Post do per_page: limit, desc: desc ), + count <- Profile.post_count_by_username(username), {:has_posts, true} <- {:has_posts, posts != []} do render(conn, :by_username, %{ posts: posts, user: user, priority: priority, - view_deleted_posts: view_deleted_posts + view_deleted_posts: view_deleted_posts, + count: count, + limit: limit, + page: page, + desc: desc }) else {:has_posts, false} -> diff --git a/lib/epochtalk_server_web/json/post_json.ex b/lib/epochtalk_server_web/json/post_json.ex index cdc11015..2dccc1e5 100644 --- a/lib/epochtalk_server_web/json/post_json.ex +++ b/lib/epochtalk_server_web/json/post_json.ex @@ -102,10 +102,18 @@ defmodule EpochtalkServerWeb.Controllers.PostJSON do @doc """ Renders all `Post` for a particular `User`. """ - def by_username(%{posts: posts, user: user, priority: priority, view_deleted_posts: view_deleted_posts}) do - posts + def by_username(%{posts: posts, user: user, priority: priority, view_deleted_posts: view_deleted_posts, count: count, limit: limit, page: page, desc: desc}) do + posts = posts |> Enum.map(&(Map.put(&1, :body_html, &1.body) |> Map.delete(:body))) |> handle_deleted_posts(nil, user, priority, view_deleted_posts) + + %{ + posts: posts, + page: page, + desc: desc, + limit: limit, + count: count + } end ## === Private Helper Functions === From 0abeb9b11c608afda3ec1f5f42211c3c52682344 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 15 Oct 2024 11:50:32 -1000 Subject: [PATCH 08/12] refactor(user-find): run mix format, resolve credo error with user find json --- lib/epochtalk_server/models/post.ex | 13 +++++++++-- lib/epochtalk_server/models/profile.ex | 11 +++++---- lib/epochtalk_server_web/controllers/post.ex | 8 ++++--- lib/epochtalk_server_web/controllers/user.ex | 6 ++--- lib/epochtalk_server_web/json/post_json.ex | 18 +++++++++++---- lib/epochtalk_server_web/json/user_json.ex | 24 ++++++++++++-------- 6 files changed, 53 insertions(+), 27 deletions(-) diff --git a/lib/epochtalk_server/models/post.ex b/lib/epochtalk_server/models/post.ex index 074dd892..49649bb4 100644 --- a/lib/epochtalk_server/models/post.ex +++ b/lib/epochtalk_server/models/post.ex @@ -440,7 +440,11 @@ defmodule EpochtalkServer.Models.Post do position: p.position, thread_id: p.thread_id, thread_slug: t.slug, - thread_title: fragment("SELECT content->>'title' as title FROM posts WHERE thread_id = ? ORDER BY id LIMIT 1", p.thread_id), + thread_title: + fragment( + "SELECT content->>'title' as title FROM posts WHERE thread_id = ? ORDER BY id LIMIT 1", + p.thread_id + ), user: %{id: p.user_id, deleted: u.deleted}, body: p.content["body"], deleted: p.deleted, @@ -448,7 +452,12 @@ defmodule EpochtalkServer.Models.Post do updated_at: p.updated_at, imported_at: p.imported_at, board_id: b.id, - board_visible: fragment("EXISTS(SELECT 1 FROM boards WHERE board_id = ? AND (viewable_by >= ? OR viewable_by IS NULL))", b.id, ^priority) + board_visible: + fragment( + "EXISTS(SELECT 1 FROM boards WHERE board_id = ? AND (viewable_by >= ? OR viewable_by IS NULL))", + b.id, + ^priority + ) }) |> Repo.all() end diff --git a/lib/epochtalk_server/models/profile.ex b/lib/epochtalk_server/models/profile.ex index 1b65e7a2..908687b4 100644 --- a/lib/epochtalk_server/models/profile.ex +++ b/lib/epochtalk_server/models/profile.ex @@ -58,11 +58,12 @@ defmodule EpochtalkServer.Models.Profile do """ @spec post_count_by_username(username :: String.t()) :: non_neg_integer() | nil def post_count_by_username(username) do - query = from p in Profile, - join: u in User, - on: p.user_id == u.id, - where: u.username == ^username, - select: p.post_count + query = + from p in Profile, + join: u in User, + on: p.user_id == u.id, + where: u.username == ^username, + select: p.post_count Repo.one(query) end diff --git a/lib/epochtalk_server_web/controllers/post.ex b/lib/epochtalk_server_web/controllers/post.ex index ccc618d7..1a29baec 100644 --- a/lib/epochtalk_server_web/controllers/post.ex +++ b/lib/epochtalk_server_web/controllers/post.ex @@ -377,15 +377,17 @@ defmodule EpochtalkServerWeb.Controllers.Post do {:user_not_deleted, user_not_deleted} <- {:user_not_deleted, User.is_active?(lookup_user.id)}, {:has_deleted_override, has_deleted_override} <- - {:has_deleted_override, ACL.has_permission(conn, "posts.pageByUser.bypass.viewDeletedUsers")}, - {:view_deleted_users, true} <- {:view_deleted_users, user_not_deleted || has_deleted_override}, + {:has_deleted_override, + ACL.has_permission(conn, "posts.pageByUser.bypass.viewDeletedUsers")}, + {:view_deleted_users, true} <- + {:view_deleted_users, user_not_deleted || has_deleted_override}, view_deleted_posts <- can_authed_user_view_deleted_posts_by_username(user), posts <- Post.page_by_username(username, priority, page, per_page: limit, desc: desc ), - count <- Profile.post_count_by_username(username), + count <- Profile.post_count_by_username(username), {:has_posts, true} <- {:has_posts, posts != []} do render(conn, :by_username, %{ posts: posts, diff --git a/lib/epochtalk_server_web/controllers/user.ex b/lib/epochtalk_server_web/controllers/user.ex index f3a11313..07f2f452 100644 --- a/lib/epochtalk_server_web/controllers/user.ex +++ b/lib/epochtalk_server_web/controllers/user.ex @@ -154,7 +154,6 @@ defmodule EpochtalkServerWeb.Controllers.User do Finds a `User`. """ def find(conn, %{"username" => username}) do - # create user with {:ok, user} <- User.by_username(username), activity <- UserActivity.get_by_user_id(user.id), metric_rank_maps <- MetricRankMap.all_merged(), @@ -163,11 +162,12 @@ defmodule EpochtalkServerWeb.Controllers.User do # Authorizations Checks :ok <- ACL.allow!(conn, "users.find"), {:user_not_deleted, user_not_deleted} <- - {:user_not_deleted, (if user.id || !user.deleted, do: true, else: false)}, + {:user_not_deleted, if(user.id || !user.deleted, do: true, else: false)}, {:has_deleted_override, has_deleted_override} <- {:has_deleted_override, ACL.has_permission(conn, "users.find.bypass.viewDeleted")}, {:view_deleted, true} <- {:view_deleted, user_not_deleted || has_deleted_override}, - {:view_as_self, view_as_self} <- {:view_as_self, authed_user && authed_user.id == user.id}, + {:view_as_self, view_as_self} <- + {:view_as_self, authed_user && authed_user.id == user.id}, {:view_as_admin, view_as_admin} <- {:view_as_admin, ACL.has_permission(conn, "users.find.bypass.viewMoreInfo")}, {:show_hidden, show_hidden} <- {:show_hidden, view_as_self || view_as_admin} do diff --git a/lib/epochtalk_server_web/json/post_json.ex b/lib/epochtalk_server_web/json/post_json.ex index 2dccc1e5..600a2a58 100644 --- a/lib/epochtalk_server_web/json/post_json.ex +++ b/lib/epochtalk_server_web/json/post_json.ex @@ -102,10 +102,20 @@ defmodule EpochtalkServerWeb.Controllers.PostJSON do @doc """ Renders all `Post` for a particular `User`. """ - def by_username(%{posts: posts, user: user, priority: priority, view_deleted_posts: view_deleted_posts, count: count, limit: limit, page: page, desc: desc}) do - posts = posts - |> Enum.map(&(Map.put(&1, :body_html, &1.body) |> Map.delete(:body))) - |> handle_deleted_posts(nil, user, priority, view_deleted_posts) + def by_username(%{ + posts: posts, + user: user, + priority: priority, + view_deleted_posts: view_deleted_posts, + count: count, + limit: limit, + page: page, + desc: desc + }) do + posts = + posts + |> Enum.map(&(Map.put(&1, :body_html, &1.body) |> Map.delete(:body))) + |> handle_deleted_posts(nil, user, priority, view_deleted_posts) %{ posts: posts, diff --git a/lib/epochtalk_server_web/json/user_json.ex b/lib/epochtalk_server_web/json/user_json.ex index 17d6f361..139d313a 100644 --- a/lib/epochtalk_server_web/json/user_json.ex +++ b/lib/epochtalk_server_web/json/user_json.ex @@ -30,20 +30,23 @@ defmodule EpochtalkServerWeb.Controllers.UserJSON do user = user |> Map.put(:profile, user.profile || %{}) user = user |> Map.put(:preferences, user.preferences || %{}) + # fetch potentially nested fields + fields = Map.get(user.profile, :fields, %{}) + ret = %{ id: user.id, username: user.username, created_at: user.created_at, updated_at: user.updated_at, avatar: Map.get(user.profile, :avatar), - post_count: Map.get(user.profile, :post_count), - last_active: Map.get(user.profile, :last_active), - dob: (if Map.get(user.profile, :fields), do: user.profile.fields["dob"]), - name: (if Map.get(user.profile, :fields), do: user.profile.fields["name"]), - gender: (if Map.get(user.profile, :fields), do: user.profile.fields["gender"]), - website: (if Map.get(user.profile, :fields), do: user.profile.fields["website"]), - language: (if Map.get(user.profile, :fields), do: user.profile.fields["language"]), - location: (if Map.get(user.profile, :fields), do: user.profile.fields["location"]), + post_count: Map.get(user.profile, :post_count), + last_active: Map.get(user.profile, :last_active), + dob: fields["dob"], + name: fields["name"], + gender: fields["gender"], + website: fields["website"], + language: fields["language"], + location: fields["location"], role_name: Map.get(highest_role, :name), role_highlight_color: Map.get(highest_role, :highlight_color), roles: Enum.map(user.roles, & &1.lookup), @@ -56,13 +59,14 @@ defmodule EpochtalkServerWeb.Controllers.UserJSON do } if show_hidden, - do: ret + do: + ret |> Map.put(:email, user.email) |> Map.put(:threads_per_page, Map.get(user.preferences, :threads_per_page) || 25) |> Map.put(:posts_per_page, Map.get(user.preferences, :posts_per_page) || 25) |> Map.put(:ignored_boards, Map.get(user.preferences, :ignored_boards) || []) |> Map.put(:collapsed_categories, Map.get(user.preferences, :collapsed_categories) || []), - else: ret + else: ret end @doc """ From 74e58c52d4378bea051293604a7dcd08891ec6fd Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 17 Oct 2024 11:02:04 -1000 Subject: [PATCH 09/12] fix(page-by-username): update spec to match function params --- lib/epochtalk_server/models/post.ex | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/epochtalk_server/models/post.ex b/lib/epochtalk_server/models/post.ex index 49649bb4..61f9246f 100644 --- a/lib/epochtalk_server/models/post.ex +++ b/lib/epochtalk_server/models/post.ex @@ -416,11 +416,12 @@ defmodule EpochtalkServer.Models.Post do @doc """ Used to page `Post` by a specific `User` given a `username` """ - @spec page_by_username(username :: String.t(), - priority :: non_neg_integer, - page :: non_neg_integer | nil, - opts :: list() | nil - ) :: [map()] | [] + @spec page_by_username( + username :: String.t(), + priority :: non_neg_integer, + page :: non_neg_integer | nil, + opts :: list() | nil + ) :: [map()] | [] def page_by_username(username, priority, page \\ 1, opts \\ []) when is_binary(username) do per_page = Keyword.get(opts, :per_page, 25) offset = page * per_page - per_page From b34afcc19d7352e199d4123589cbd0bbc81fbb79 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 17 Oct 2024 12:49:25 -1000 Subject: [PATCH 10/12] feat(page-threads-by-username): create function to page threads by username --- lib/epochtalk_server/models/thread.ex | 50 +++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/lib/epochtalk_server/models/thread.ex b/lib/epochtalk_server/models/thread.ex index 4c2d7ba0..ca57f578 100644 --- a/lib/epochtalk_server/models/thread.ex +++ b/lib/epochtalk_server/models/thread.ex @@ -571,6 +571,56 @@ defmodule EpochtalkServer.Models.Thread do end end + @doc """ + Used to page all `Thread`s by a specific `User` given a `username` + """ + @spec page_by_username( + username :: String.t(), + priority :: non_neg_integer, + page :: non_neg_integer | nil, + opts :: list() | nil + ) :: [map()] | [] + def page_by_username(username, priority, page \\ 1, opts \\ []) when is_binary(username) do + per_page = Keyword.get(opts, :per_page, 25) + offset = page * per_page - per_page + desc = Keyword.get(opts, :desc, true) == true + direction = if desc, do: :desc, else: :asc + + Post + |> join(:left, [p], t in Thread, on: t.id == p.thread_id) + |> join(:left, [p], u in User, on: u.id == p.user_id) + |> join(:left, [p, t], b in Board, on: b.id == t.board_id) + |> where([p, t, u], u.username == ^username and p.user_id == u.id and p.position == 1) + |> order_by([p], [{^direction, p.id}]) + |> limit(^per_page) + |> offset(^offset) + |> select([p, t, u, b], %{ + id: p.id, + position: p.position, + thread_id: p.thread_id, + thread_slug: t.slug, + thread_title: + fragment( + "SELECT content->>'title' as title FROM posts WHERE thread_id = ? ORDER BY id LIMIT 1", + p.thread_id + ), + user: %{id: p.user_id, deleted: u.deleted}, + body: p.content["body"], + deleted: p.deleted, + created_at: p.created_at, + updated_at: p.updated_at, + imported_at: p.imported_at, + board_id: b.id, + board_visible: + fragment( + "EXISTS(SELECT 1 FROM boards WHERE board_id = ? AND (viewable_by >= ? OR viewable_by IS NULL))", + b.id, + ^priority + ) + }) + |> Repo.all() + end + @doc """ Returns a specific `Thread` given a valid `id` or `slug` """ From 02bef0eed8f05f931fa794cb5055374b843c3844 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Fri, 18 Oct 2024 09:52:07 -1000 Subject: [PATCH 11/12] feat(threads-by-username): implement route for retreiving threads by username --- lib/epochtalk_server_web/controllers/post.ex | 34 ++++++------ .../controllers/thread.ex | 53 +++++++++++++++++++ lib/epochtalk_server_web/json/post_json.ex | 14 +++-- lib/epochtalk_server_web/json/thread_json.ex | 35 ++++++++++++ lib/epochtalk_server_web/router.ex | 1 + 5 files changed, 116 insertions(+), 21 deletions(-) diff --git a/lib/epochtalk_server_web/controllers/post.ex b/lib/epochtalk_server_web/controllers/post.ex index 1a29baec..e4d25a74 100644 --- a/lib/epochtalk_server_web/controllers/post.ex +++ b/lib/epochtalk_server_web/controllers/post.ex @@ -427,50 +427,52 @@ defmodule EpochtalkServerWeb.Controllers.Post do end end - ## === Private Authorization Helper Functions === + ## === Public Authorization Helper Functions === - defp can_authed_user_view_deleted_posts(nil, _thread_id), do: false + def can_authed_user_view_deleted_posts_by_username(nil), do: false - defp can_authed_user_view_deleted_posts(user, thread_id) do - view_all = ACL.has_permission(user, "posts.byThread.bypass.viewDeletedPosts.admin") - view_some = ACL.has_permission(user, "posts.byThread.bypass.viewDeletedPosts.mod") - view_self_mod = ACL.has_permission(user, "posts.byThread.bypass.viewDeletedPosts.selfMod") - view_priority = ACL.has_permission(user, "posts.byThread.bypass.viewDeletedPosts.priority") + def can_authed_user_view_deleted_posts_by_username(user) do + view_all = ACL.has_permission(user, "posts.pageByUser.bypass.viewDeletedPosts.admin") + view_some = ACL.has_permission(user, "posts.pageByUser.bypass.viewDeletedPosts.mod") user_id = Map.get(user, :id) moderated_boards = BoardModerator.get_user_moderated_boards(user_id) cond do - view_all or view_priority -> + view_all -> true view_some and moderated_boards != [] -> moderated_boards - view_self_mod and moderated_boards == [] -> - Thread.self_moderated_by_user?(thread_id, user_id) - true -> false end end - defp can_authed_user_view_deleted_posts_by_username(nil), do: false + ## === Private Authorization Helper Functions === - defp can_authed_user_view_deleted_posts_by_username(user) do - view_all = ACL.has_permission(user, "posts.byUsername.bypass.viewDeletedPosts.admin") - view_some = ACL.has_permission(user, "posts.byUsername.bypass.viewDeletedPosts.mod") + defp can_authed_user_view_deleted_posts(nil, _thread_id), do: false + + defp can_authed_user_view_deleted_posts(user, thread_id) do + view_all = ACL.has_permission(user, "posts.byThread.bypass.viewDeletedPosts.admin") + view_some = ACL.has_permission(user, "posts.byThread.bypass.viewDeletedPosts.mod") + view_self_mod = ACL.has_permission(user, "posts.byThread.bypass.viewDeletedPosts.selfMod") + view_priority = ACL.has_permission(user, "posts.byThread.bypass.viewDeletedPosts.priority") user_id = Map.get(user, :id) moderated_boards = BoardModerator.get_user_moderated_boards(user_id) cond do - view_all -> + view_all or view_priority -> true view_some and moderated_boards != [] -> moderated_boards + view_self_mod and moderated_boards == [] -> + Thread.self_moderated_by_user?(thread_id, user_id) + true -> false end diff --git a/lib/epochtalk_server_web/controllers/thread.ex b/lib/epochtalk_server_web/controllers/thread.ex index 5f134908..6edaf19a 100644 --- a/lib/epochtalk_server_web/controllers/thread.ex +++ b/lib/epochtalk_server_web/controllers/thread.ex @@ -225,6 +225,59 @@ defmodule EpochtalkServerWeb.Controllers.Thread do end end + @doc """ + Used to retrieve `Threads` for a `User` by username + """ + def by_username(conn, attrs) do + # Parameter Validation + with username <- attrs["username"], + page <- Validate.cast(attrs, "page", :integer, default: 1, min: 1), + limit <- Validate.cast(attrs, "limit", :integer, default: 25, min: 1, max: 100), + desc <- Validate.cast(attrs, "desc", :boolean, default: true), + user <- Guardian.Plug.current_resource(conn), + priority <- ACL.get_user_priority(conn), + [lookup_user] <- User.ids_from_usernames([username]), + + # Authorizations Checks (Same permission as post page by user) + :ok <- ACL.allow!(conn, "posts.pageByUser"), + {:user_not_deleted, user_not_deleted} <- + {:user_not_deleted, User.is_active?(lookup_user.id)}, + {:has_deleted_override, has_deleted_override} <- + {:has_deleted_override, + ACL.has_permission(conn, "posts.pageFirstPostByUser.bypass.viewDeletedUsers")}, + {:view_deleted_users, true} <- + {:view_deleted_users, user_not_deleted || has_deleted_override}, + view_deleted_threads <- + EpochtalkServerWeb.Controllers.Post.can_authed_user_view_deleted_posts_by_username( + user + ), + threads <- + Thread.page_by_username(username, priority, page, + per_page: limit + 1, + desc: desc + ), + {:has_threads, true} <- {:has_threads, threads != []} do + render(conn, :by_username, %{ + threads: threads, + user: user, + priority: priority, + view_deleted_threads: view_deleted_threads, + limit: limit, + desc: desc, + page: page + }) + else + {:has_threads, false} -> + ErrorHelpers.render_json_error(conn, 404, "Error, requested threads not found") + + {:view_deleted_users, false} -> + ErrorHelpers.render_json_error(conn, 400, "Account not found") + + _ -> + ErrorHelpers.render_json_error(conn, 400, "Error, cannot get threads by username") + end + end + @doc """ Used to watch `Thread` """ diff --git a/lib/epochtalk_server_web/json/post_json.ex b/lib/epochtalk_server_web/json/post_json.ex index 600a2a58..1b3f3794 100644 --- a/lib/epochtalk_server_web/json/post_json.ex +++ b/lib/epochtalk_server_web/json/post_json.ex @@ -126,9 +126,9 @@ defmodule EpochtalkServerWeb.Controllers.PostJSON do } end - ## === Private Helper Functions === + ## === Public Helper Functions === - defp handle_deleted_posts(posts, thread, user, authed_user_priority, view_deleted_posts) do + def handle_deleted_posts(posts, thread, user, authed_user_priority, view_deleted_posts) do authed_user_id = if is_nil(user), do: nil, else: user.id has_self_mod_bypass = @@ -163,6 +163,8 @@ defmodule EpochtalkServerWeb.Controllers.PostJSON do cleaned_posts end + ## === Private Helper Functions === + defp handle_deleted_post( post, authed_user_id, @@ -186,9 +188,11 @@ defmodule EpochtalkServerWeb.Controllers.PostJSON do # check if user has priority to view hidden post, # or if the user was the one who hid the post - authed_user_has_priority = if is_nil(post_hidden_by_priority), - do: false, - else: authed_user_priority <= post_hidden_by_priority + authed_user_has_priority = + if is_nil(post_hidden_by_priority), + do: false, + else: authed_user_priority <= post_hidden_by_priority + authed_user_hid_post = post_hidden_by_id == authed_user_id post_is_viewable = diff --git a/lib/epochtalk_server_web/json/thread_json.ex b/lib/epochtalk_server_web/json/thread_json.ex index 013be13a..8cef973b 100644 --- a/lib/epochtalk_server_web/json/thread_json.ex +++ b/lib/epochtalk_server_web/json/thread_json.ex @@ -1,5 +1,6 @@ defmodule EpochtalkServerWeb.Controllers.ThreadJSON do alias EpochtalkServerWeb.Controllers.BoardJSON + alias EpochtalkServerWeb.Controllers.PostJSON @moduledoc """ Renders and formats `Thread` data, in JSON format for frontend @@ -127,6 +128,40 @@ defmodule EpochtalkServerWeb.Controllers.ThreadJSON do if board_banned, do: Map.put(result, :board_banned, board_banned), else: result end + @doc """ + Renders paged `Threads` for a particular `User`. + """ + def by_username(%{ + threads: threads, + user: user, + priority: priority, + view_deleted_threads: view_deleted_threads, + limit: limit, + desc: desc, + page: page + }) do + next = length(threads) > limit + + threads = + if next, + do: threads |> Enum.reverse() |> tl() |> Enum.reverse(), + else: threads + + threads = + threads + |> Enum.map(&(Map.put(&1, :body_html, &1.body) |> Map.delete(:body))) + |> PostJSON.handle_deleted_posts(nil, user, priority, view_deleted_threads) + + %{ + posts: threads, + desc: desc, + limit: limit, + page: page, + next: next, + prev: page > 1 + } + end + @doc """ Renders sticky `Thread`. diff --git a/lib/epochtalk_server_web/router.ex b/lib/epochtalk_server_web/router.ex index 2991a62e..dcdb48d2 100644 --- a/lib/epochtalk_server_web/router.ex +++ b/lib/epochtalk_server_web/router.ex @@ -76,6 +76,7 @@ defmodule EpochtalkServerWeb.Router do get "/users/:username", User, :find get "/posts", Post, :by_thread get "/posts/user/:username", Post, :by_username + get "/threads/user/:username", Thread, :by_username get "/threads", Thread, :by_board get "/threads/:slug/id", Thread, :slug_to_id post "/threads/:id/viewed", Thread, :viewed From 699b2af015de42e0dbee95d6a6f87f47cd1cf244 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 21 Oct 2024 12:16:17 -1000 Subject: [PATCH 12/12] fix(profile): allow user to have no posts/threads when querying posts/threads by username --- lib/epochtalk_server_web/controllers/post.ex | 6 +----- lib/epochtalk_server_web/controllers/thread.ex | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/epochtalk_server_web/controllers/post.ex b/lib/epochtalk_server_web/controllers/post.ex index e4d25a74..c1d62b72 100644 --- a/lib/epochtalk_server_web/controllers/post.ex +++ b/lib/epochtalk_server_web/controllers/post.ex @@ -387,8 +387,7 @@ defmodule EpochtalkServerWeb.Controllers.Post do per_page: limit, desc: desc ), - count <- Profile.post_count_by_username(username), - {:has_posts, true} <- {:has_posts, posts != []} do + count <- Profile.post_count_by_username(username) do render(conn, :by_username, %{ posts: posts, user: user, @@ -400,9 +399,6 @@ defmodule EpochtalkServerWeb.Controllers.Post do desc: desc }) else - {:has_posts, false} -> - ErrorHelpers.render_json_error(conn, 404, "Error, requested posts not found") - {:view_deleted_users, false} -> ErrorHelpers.render_json_error(conn, 400, "Account not found") diff --git a/lib/epochtalk_server_web/controllers/thread.ex b/lib/epochtalk_server_web/controllers/thread.ex index 6edaf19a..19694cad 100644 --- a/lib/epochtalk_server_web/controllers/thread.ex +++ b/lib/epochtalk_server_web/controllers/thread.ex @@ -255,8 +255,7 @@ defmodule EpochtalkServerWeb.Controllers.Thread do Thread.page_by_username(username, priority, page, per_page: limit + 1, desc: desc - ), - {:has_threads, true} <- {:has_threads, threads != []} do + ) do render(conn, :by_username, %{ threads: threads, user: user, @@ -267,9 +266,6 @@ defmodule EpochtalkServerWeb.Controllers.Thread do page: page }) else - {:has_threads, false} -> - ErrorHelpers.render_json_error(conn, 404, "Error, requested threads not found") - {:view_deleted_users, false} -> ErrorHelpers.render_json_error(conn, 400, "Account not found")