Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User find #115

Merged
merged 12 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading