Skip to content

Commit 669c3d8

Browse files
Merge pull request #115 from epochtalk/user-find
User find
2 parents 695f185 + 02bef0e commit 669c3d8

File tree

10 files changed

+426
-6
lines changed

10 files changed

+426
-6
lines changed

lib/epochtalk_server/models/post.ex

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ defmodule EpochtalkServer.Models.Post do
33
import Ecto.Changeset
44
import Ecto.Query
55
alias EpochtalkServer.Repo
6+
alias EpochtalkServer.Models.Board
67
alias EpochtalkServer.Models.Post
78
alias EpochtalkServer.Models.Thread
89
alias EpochtalkServer.Models.User
@@ -412,6 +413,56 @@ defmodule EpochtalkServer.Models.Post do
412413
else: results
413414
end
414415

416+
@doc """
417+
Used to page `Post` by a specific `User` given a `username`
418+
"""
419+
@spec page_by_username(
420+
username :: String.t(),
421+
priority :: non_neg_integer,
422+
page :: non_neg_integer | nil,
423+
opts :: list() | nil
424+
) :: [map()] | []
425+
def page_by_username(username, priority, page \\ 1, opts \\ []) when is_binary(username) do
426+
per_page = Keyword.get(opts, :per_page, 25)
427+
offset = page * per_page - per_page
428+
desc = Keyword.get(opts, :desc, true) == true
429+
direction = if desc, do: :desc, else: :asc
430+
431+
Post
432+
|> join(:left, [p], t in Thread, on: t.id == p.thread_id)
433+
|> join(:left, [p], u in User, on: u.id == p.user_id)
434+
|> join(:left, [p, t], b in Board, on: b.id == t.board_id)
435+
|> where([p, t, u], u.username == ^username and p.user_id == u.id)
436+
|> order_by([p], [{^direction, p.id}])
437+
|> limit(^per_page)
438+
|> offset(^offset)
439+
|> select([p, t, u, b], %{
440+
id: p.id,
441+
position: p.position,
442+
thread_id: p.thread_id,
443+
thread_slug: t.slug,
444+
thread_title:
445+
fragment(
446+
"SELECT content->>'title' as title FROM posts WHERE thread_id = ? ORDER BY id LIMIT 1",
447+
p.thread_id
448+
),
449+
user: %{id: p.user_id, deleted: u.deleted},
450+
body: p.content["body"],
451+
deleted: p.deleted,
452+
created_at: p.created_at,
453+
updated_at: p.updated_at,
454+
imported_at: p.imported_at,
455+
board_id: b.id,
456+
board_visible:
457+
fragment(
458+
"EXISTS(SELECT 1 FROM boards WHERE board_id = ? AND (viewable_by >= ? OR viewable_by IS NULL))",
459+
b.id,
460+
^priority
461+
)
462+
})
463+
|> Repo.all()
464+
end
465+
415466
@doc """
416467
Used to correct the text search vector for post after being modified for mentions
417468
"""

lib/epochtalk_server/models/profile.ex

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,21 @@ defmodule EpochtalkServer.Models.Profile do
5353

5454
## === Database Functions ===
5555

56+
@doc """
57+
Gets the `post_count` field given a `User` username
58+
"""
59+
@spec post_count_by_username(username :: String.t()) :: non_neg_integer() | nil
60+
def post_count_by_username(username) do
61+
query =
62+
from p in Profile,
63+
join: u in User,
64+
on: p.user_id == u.id,
65+
where: u.username == ^username,
66+
select: p.post_count
67+
68+
Repo.one(query)
69+
end
70+
5671
@doc """
5772
Increments the `post_count` field given a `User` id
5873
"""

lib/epochtalk_server/models/thread.ex

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,56 @@ defmodule EpochtalkServer.Models.Thread do
571571
end
572572
end
573573

574+
@doc """
575+
Used to page all `Thread`s by a specific `User` given a `username`
576+
"""
577+
@spec page_by_username(
578+
username :: String.t(),
579+
priority :: non_neg_integer,
580+
page :: non_neg_integer | nil,
581+
opts :: list() | nil
582+
) :: [map()] | []
583+
def page_by_username(username, priority, page \\ 1, opts \\ []) when is_binary(username) do
584+
per_page = Keyword.get(opts, :per_page, 25)
585+
offset = page * per_page - per_page
586+
desc = Keyword.get(opts, :desc, true) == true
587+
direction = if desc, do: :desc, else: :asc
588+
589+
Post
590+
|> join(:left, [p], t in Thread, on: t.id == p.thread_id)
591+
|> join(:left, [p], u in User, on: u.id == p.user_id)
592+
|> join(:left, [p, t], b in Board, on: b.id == t.board_id)
593+
|> where([p, t, u], u.username == ^username and p.user_id == u.id and p.position == 1)
594+
|> order_by([p], [{^direction, p.id}])
595+
|> limit(^per_page)
596+
|> offset(^offset)
597+
|> select([p, t, u, b], %{
598+
id: p.id,
599+
position: p.position,
600+
thread_id: p.thread_id,
601+
thread_slug: t.slug,
602+
thread_title:
603+
fragment(
604+
"SELECT content->>'title' as title FROM posts WHERE thread_id = ? ORDER BY id LIMIT 1",
605+
p.thread_id
606+
),
607+
user: %{id: p.user_id, deleted: u.deleted},
608+
body: p.content["body"],
609+
deleted: p.deleted,
610+
created_at: p.created_at,
611+
updated_at: p.updated_at,
612+
imported_at: p.imported_at,
613+
board_id: b.id,
614+
board_visible:
615+
fragment(
616+
"EXISTS(SELECT 1 FROM boards WHERE board_id = ? AND (viewable_by >= ? OR viewable_by IS NULL))",
617+
b.id,
618+
^priority
619+
)
620+
})
621+
|> Repo.all()
622+
end
623+
574624
@doc """
575625
Returns a specific `Thread` given a valid `id` or `slug`
576626
"""

lib/epochtalk_server_web/controllers/post.ex

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ defmodule EpochtalkServerWeb.Controllers.Post do
1010
alias EpochtalkServerWeb.Helpers.ACL
1111
alias EpochtalkServerWeb.Helpers.Sanitize
1212
alias EpochtalkServerWeb.Helpers.Parse
13+
alias EpochtalkServer.Models.Profile
1314
alias EpochtalkServer.Models.Post
1415
alias EpochtalkServer.Models.Poll
1516
alias EpochtalkServer.Models.Thread
@@ -358,6 +359,58 @@ defmodule EpochtalkServerWeb.Controllers.Post do
358359
end
359360
end
360361

362+
@doc """
363+
Used to retrieve `Posts` for a `User` by username
364+
"""
365+
def by_username(conn, attrs) do
366+
# Parameter Validation
367+
with username <- attrs["username"],
368+
page <- Validate.cast(attrs, "page", :integer, default: 1, min: 1),
369+
limit <- Validate.cast(attrs, "limit", :integer, default: 25, min: 1, max: 100),
370+
desc <- Validate.cast(attrs, "desc", :boolean, default: true),
371+
user <- Guardian.Plug.current_resource(conn),
372+
priority <- ACL.get_user_priority(conn),
373+
[lookup_user] <- User.ids_from_usernames([username]),
374+
375+
# Authorizations Checks
376+
:ok <- ACL.allow!(conn, "posts.pageByUser"),
377+
{:user_not_deleted, user_not_deleted} <-
378+
{:user_not_deleted, User.is_active?(lookup_user.id)},
379+
{:has_deleted_override, has_deleted_override} <-
380+
{:has_deleted_override,
381+
ACL.has_permission(conn, "posts.pageByUser.bypass.viewDeletedUsers")},
382+
{:view_deleted_users, true} <-
383+
{:view_deleted_users, user_not_deleted || has_deleted_override},
384+
view_deleted_posts <- can_authed_user_view_deleted_posts_by_username(user),
385+
posts <-
386+
Post.page_by_username(username, priority, page,
387+
per_page: limit,
388+
desc: desc
389+
),
390+
count <- Profile.post_count_by_username(username),
391+
{:has_posts, true} <- {:has_posts, posts != []} do
392+
render(conn, :by_username, %{
393+
posts: posts,
394+
user: user,
395+
priority: priority,
396+
view_deleted_posts: view_deleted_posts,
397+
count: count,
398+
limit: limit,
399+
page: page,
400+
desc: desc
401+
})
402+
else
403+
{:has_posts, false} ->
404+
ErrorHelpers.render_json_error(conn, 404, "Error, requested posts not found")
405+
406+
{:view_deleted_users, false} ->
407+
ErrorHelpers.render_json_error(conn, 400, "Account not found")
408+
409+
_ ->
410+
ErrorHelpers.render_json_error(conn, 400, "Error, cannot get posts by username")
411+
end
412+
end
413+
361414
@doc """
362415
Get `Post` preview by running content through parser
363416
"""
@@ -374,6 +427,29 @@ defmodule EpochtalkServerWeb.Controllers.Post do
374427
end
375428
end
376429

430+
## === Public Authorization Helper Functions ===
431+
432+
def can_authed_user_view_deleted_posts_by_username(nil), do: false
433+
434+
def can_authed_user_view_deleted_posts_by_username(user) do
435+
view_all = ACL.has_permission(user, "posts.pageByUser.bypass.viewDeletedPosts.admin")
436+
view_some = ACL.has_permission(user, "posts.pageByUser.bypass.viewDeletedPosts.mod")
437+
438+
user_id = Map.get(user, :id)
439+
moderated_boards = BoardModerator.get_user_moderated_boards(user_id)
440+
441+
cond do
442+
view_all ->
443+
true
444+
445+
view_some and moderated_boards != [] ->
446+
moderated_boards
447+
448+
true ->
449+
false
450+
end
451+
end
452+
377453
## === Private Authorization Helper Functions ===
378454

379455
defp can_authed_user_view_deleted_posts(nil, _thread_id), do: false

lib/epochtalk_server_web/controllers/thread.ex

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,59 @@ defmodule EpochtalkServerWeb.Controllers.Thread do
225225
end
226226
end
227227

228+
@doc """
229+
Used to retrieve `Threads` for a `User` by username
230+
"""
231+
def by_username(conn, attrs) do
232+
# Parameter Validation
233+
with username <- attrs["username"],
234+
page <- Validate.cast(attrs, "page", :integer, default: 1, min: 1),
235+
limit <- Validate.cast(attrs, "limit", :integer, default: 25, min: 1, max: 100),
236+
desc <- Validate.cast(attrs, "desc", :boolean, default: true),
237+
user <- Guardian.Plug.current_resource(conn),
238+
priority <- ACL.get_user_priority(conn),
239+
[lookup_user] <- User.ids_from_usernames([username]),
240+
241+
# Authorizations Checks (Same permission as post page by user)
242+
:ok <- ACL.allow!(conn, "posts.pageByUser"),
243+
{:user_not_deleted, user_not_deleted} <-
244+
{:user_not_deleted, User.is_active?(lookup_user.id)},
245+
{:has_deleted_override, has_deleted_override} <-
246+
{:has_deleted_override,
247+
ACL.has_permission(conn, "posts.pageFirstPostByUser.bypass.viewDeletedUsers")},
248+
{:view_deleted_users, true} <-
249+
{:view_deleted_users, user_not_deleted || has_deleted_override},
250+
view_deleted_threads <-
251+
EpochtalkServerWeb.Controllers.Post.can_authed_user_view_deleted_posts_by_username(
252+
user
253+
),
254+
threads <-
255+
Thread.page_by_username(username, priority, page,
256+
per_page: limit + 1,
257+
desc: desc
258+
),
259+
{:has_threads, true} <- {:has_threads, threads != []} do
260+
render(conn, :by_username, %{
261+
threads: threads,
262+
user: user,
263+
priority: priority,
264+
view_deleted_threads: view_deleted_threads,
265+
limit: limit,
266+
desc: desc,
267+
page: page
268+
})
269+
else
270+
{:has_threads, false} ->
271+
ErrorHelpers.render_json_error(conn, 404, "Error, requested threads not found")
272+
273+
{:view_deleted_users, false} ->
274+
ErrorHelpers.render_json_error(conn, 400, "Account not found")
275+
276+
_ ->
277+
ErrorHelpers.render_json_error(conn, 400, "Error, cannot get threads by username")
278+
end
279+
end
280+
228281
@doc """
229282
Used to watch `Thread`
230283
"""

lib/epochtalk_server_web/controllers/user.ex

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@ defmodule EpochtalkServerWeb.Controllers.User do
55
Controller For `User` related API requests
66
"""
77
alias EpochtalkServer.Models.User
8+
alias EpochtalkServer.Models.UserActivity
89
alias EpochtalkServer.Models.Ban
10+
alias EpochtalkServer.Models.MetricRankMap
11+
alias EpochtalkServer.Models.Rank
912
alias EpochtalkServer.Models.Invitation
1013
alias EpochtalkServer.Auth.Guardian
1114
alias EpochtalkServer.Session
1215
alias EpochtalkServer.Mailer
1316
alias EpochtalkServerWeb.ErrorHelpers
1417
alias EpochtalkServerWeb.CustomErrors.InvalidPayload
18+
alias EpochtalkServerWeb.Helpers.ACL
1519
alias EpochtalkServerWeb.Helpers.Validate
1620

1721
@doc """
@@ -146,6 +150,51 @@ defmodule EpochtalkServerWeb.Controllers.User do
146150

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

153+
@doc """
154+
Finds a `User`.
155+
"""
156+
def find(conn, %{"username" => username}) do
157+
with {:ok, user} <- User.by_username(username),
158+
activity <- UserActivity.get_by_user_id(user.id),
159+
metric_rank_maps <- MetricRankMap.all_merged(),
160+
ranks <- Rank.all(),
161+
authed_user <- Guardian.Plug.current_resource(conn),
162+
# Authorizations Checks
163+
:ok <- ACL.allow!(conn, "users.find"),
164+
{:user_not_deleted, user_not_deleted} <-
165+
{:user_not_deleted, if(user.id || !user.deleted, do: true, else: false)},
166+
{:has_deleted_override, has_deleted_override} <-
167+
{:has_deleted_override, ACL.has_permission(conn, "users.find.bypass.viewDeleted")},
168+
{:view_deleted, true} <- {:view_deleted, user_not_deleted || has_deleted_override},
169+
{:view_as_self, view_as_self} <-
170+
{:view_as_self, authed_user && authed_user.id == user.id},
171+
{:view_as_admin, view_as_admin} <-
172+
{:view_as_admin, ACL.has_permission(conn, "users.find.bypass.viewMoreInfo")},
173+
{:show_hidden, show_hidden} <- {:show_hidden, view_as_self || view_as_admin} do
174+
render(conn, :find, %{
175+
user: user,
176+
activity: activity,
177+
metric_rank_maps: metric_rank_maps,
178+
ranks: ranks,
179+
show_hidden: show_hidden
180+
})
181+
else
182+
{:error, :user_not_found} ->
183+
ErrorHelpers.render_json_error(conn, 400, "Account not found")
184+
185+
{:error, data} ->
186+
ErrorHelpers.render_json_error(conn, 400, data)
187+
188+
{:view_deleted, false} ->
189+
ErrorHelpers.render_json_error(conn, 400, "Account not found")
190+
191+
_ ->
192+
ErrorHelpers.render_json_error(conn, 500, "There was an issue finding user")
193+
end
194+
end
195+
196+
def find(_conn, _attrs), do: raise(InvalidPayload)
197+
149198
@doc """
150199
Authenticates currently logged in `User`
151200
"""

0 commit comments

Comments
 (0)