Skip to content

Commit

Permalink
Merge pull request #115 from epochtalk/user-find
Browse files Browse the repository at this point in the history
User find
  • Loading branch information
unenglishable authored Oct 18, 2024
2 parents 695f185 + 02bef0e commit 669c3d8
Show file tree
Hide file tree
Showing 10 changed files with 426 additions and 6 deletions.
51 changes: 51 additions & 0 deletions lib/epochtalk_server/models/post.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -412,6 +413,56 @@ 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
"""
Expand Down
15 changes: 15 additions & 0 deletions lib/epochtalk_server/models/profile.ex
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,21 @@ 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
"""
Expand Down
50 changes: 50 additions & 0 deletions lib/epochtalk_server/models/thread.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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`
"""
Expand Down
76 changes: 76 additions & 0 deletions lib/epochtalk_server_web/controllers/post.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -358,6 +359,58 @@ 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
),
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,
count: count,
limit: limit,
page: page,
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")

_ ->
ErrorHelpers.render_json_error(conn, 400, "Error, cannot get posts by username")
end
end

@doc """
Get `Post` preview by running content through parser
"""
Expand All @@ -374,6 +427,29 @@ defmodule EpochtalkServerWeb.Controllers.Post do
end
end

## === Public Authorization Helper Functions ===

def can_authed_user_view_deleted_posts_by_username(nil), do: false

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 ->
true

view_some and moderated_boards != [] ->
moderated_boards

true ->
false
end
end

## === Private Authorization Helper Functions ===

defp can_authed_user_view_deleted_posts(nil, _thread_id), do: false
Expand Down
53 changes: 53 additions & 0 deletions lib/epochtalk_server_web/controllers/thread.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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`
"""
Expand Down
49 changes: 49 additions & 0 deletions lib/epochtalk_server_web/controllers/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ 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.MetricRankMap
alias EpochtalkServer.Models.Rank
alias EpochtalkServer.Models.Invitation
alias EpochtalkServer.Auth.Guardian
alias EpochtalkServer.Session
alias EpochtalkServer.Mailer
alias EpochtalkServerWeb.ErrorHelpers
alias EpochtalkServerWeb.CustomErrors.InvalidPayload
alias EpochtalkServerWeb.Helpers.ACL
alias EpochtalkServerWeb.Helpers.Validate

@doc """
Expand Down Expand Up @@ -146,6 +150,51 @@ defmodule EpochtalkServerWeb.Controllers.User do

def confirm(_conn, _attrs), do: raise(InvalidPayload)

@doc """
Finds a `User`.
"""
def find(conn, %{"username" => username}) 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(),
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,
show_hidden: show_hidden
})
else
{:error, :user_not_found} ->
ErrorHelpers.render_json_error(conn, 400, "Account not found")

{: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
end

def find(_conn, _attrs), do: raise(InvalidPayload)

@doc """
Authenticates currently logged in `User`
"""
Expand Down
Loading

0 comments on commit 669c3d8

Please sign in to comment.