From ac2db54b6c4b7b99d164d3a5d1ab4145218882e5 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Tue, 6 Sep 2022 11:23:55 -1000 Subject: [PATCH 001/231] feat(mix): add redix dependency --- mix.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/mix.exs b/mix.exs index 31d1e3f5..d73549f2 100644 --- a/mix.exs +++ b/mix.exs @@ -45,6 +45,7 @@ defmodule EpochtalkServer.MixProject do {:phoenix_ecto, "~> 4.4"}, {:plug_cowboy, "~> 2.5"}, {:postgrex, ">= 0.0.0"}, + {:redix, "~> 1.1.5"}, {:swoosh, "~> 1.3"}, {:telemetry_metrics, "~> 0.6"}, {:telemetry_poller, "~> 1.0"}, From 31651b171c24310e0a15662050e652433a1aeba4 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Tue, 6 Sep 2022 11:24:09 -1000 Subject: [PATCH 002/231] feat(application): add redix as app child --- lib/epochtalk_server/application.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/epochtalk_server/application.ex b/lib/epochtalk_server/application.ex index daf542fc..ea57355b 100644 --- a/lib/epochtalk_server/application.ex +++ b/lib/epochtalk_server/application.ex @@ -10,6 +10,8 @@ defmodule EpochtalkServer.Application do children = [ # Start Guardian Redis Redix connection GuardianRedis.Redix, + # Start the server Redis connection + {Redix, host: "localhost", name: :redix}, # Start the Ecto repository EpochtalkServer.Repo, # Start the Telemetry supervisor From 35ba0c5878fa6fd9f91cbbc43249d390698bdd65 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Tue, 6 Sep 2022 11:24:48 -1000 Subject: [PATCH 003/231] docs(guardian): add todo for redis call in resource_from_claims --- lib/epochtalk_server/auth/guardian.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/epochtalk_server/auth/guardian.ex b/lib/epochtalk_server/auth/guardian.ex index 69dd54d6..9996cdf5 100644 --- a/lib/epochtalk_server/auth/guardian.ex +++ b/lib/epochtalk_server/auth/guardian.ex @@ -21,6 +21,7 @@ defmodule EpochtalkServer.Auth.Guardian do # the resource id so here we'll rely on that to look it up. resource = user_id |> String.to_integer + # TODO: redis call here instead |> User.by_id {:ok, resource} end From 1ad6cfed9b204c4f58bdb30adbbb477605397506 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Tue, 6 Sep 2022 11:26:02 -1000 Subject: [PATCH 004/231] feat(session): stub session.ex contains methods from server/plugins/session from old js project --- lib/epochtalk_server/session.ex | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 lib/epochtalk_server/session.ex diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex new file mode 100644 index 00000000..d5dc5175 --- /dev/null +++ b/lib/epochtalk_server/session.ex @@ -0,0 +1,17 @@ +defmodule EpochtalkServer.Session do + def save do + + end + def update_roles do + + end + def update_moderating do + + end + def update_user_info do + + end + def update_ban_info do + + end +end From 65dbae181895e6f5798a5efffa9fae03dc51997d Mon Sep 17 00:00:00 2001 From: unenglishable Date: Tue, 6 Sep 2022 15:09:13 -1000 Subject: [PATCH 005/231] feat(session.save): set default role if necessary --- lib/epochtalk_server/session.ex | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index d5dc5175..afedcef6 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -1,6 +1,17 @@ defmodule EpochtalkServer.Session do - def save do + def save(db_user) do + # replace db_user role with role lookups + # |> Map.put(:role, Enum.map(db_user.roles, &(&1.lookup)) + # actually, just assume role lookups are there + # set default role if necessary + roles = db_user.roles + |> List.length + |> case do + 0 -> ['user'] + _ -> db_user.roles + end + db_user = Map.put(db_user, :roles, roles) end def update_roles do From f3ae1463f7d06366fda093cfeb421d4648a6ddca Mon Sep 17 00:00:00 2001 From: unenglishable Date: Tue, 6 Sep 2022 15:10:10 -1000 Subject: [PATCH 006/231] feat(session.save): save username, avatar to redis hash --- lib/epochtalk_server/session.ex | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index afedcef6..235d2b39 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -12,6 +12,14 @@ defmodule EpochtalkServer.Session do _ -> db_user.roles end db_user = Map.put(db_user, :roles, roles) + + # save username, avatar to redis hash under "user:{userId}" + user_key = "user:#{db_user.id}" + case db_user.avatar do + "" -> Redix.command(:redix, ["HSET", user_key, "username", db_user.username]) + # set avatar if it's available + _ -> Redix.command(:redix, ["HSET", user_key, "username", db_user.username, "avatar", db_user.avatar]) + end end def update_roles do From 40c18b20bfa621a9ca52a57bbb145f227bd23c21 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Tue, 6 Sep 2022 15:10:35 -1000 Subject: [PATCH 007/231] feat(session.save): save/replace roles to redis --- lib/epochtalk_server/session.ex | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index 235d2b39..4e1e6765 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -20,6 +20,11 @@ defmodule EpochtalkServer.Session do # set avatar if it's available _ -> Redix.command(:redix, ["HSET", user_key, "username", db_user.username, "avatar", db_user.avatar]) end + + # save/replace roles to redis under "user:{userId}:roles" + role_key = "user:#{db_user.id}:roles" + Redix.command(:redix, ["DEL", role_key]) + Redix.command(:redix, ["SADD", role_key, db_user.roles]) end def update_roles do From b110b03d021cecb6000a2ad5cdc2576a1ab05a05 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Tue, 6 Sep 2022 15:11:07 -1000 Subject: [PATCH 008/231] feat(session.save): save/replace ban_expiration to redis --- lib/epochtalk_server/session.ex | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index 4e1e6765..5e3e77e5 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -25,6 +25,17 @@ defmodule EpochtalkServer.Session do role_key = "user:#{db_user.id}:roles" Redix.command(:redix, ["DEL", role_key]) Redix.command(:redix, ["SADD", role_key, db_user.roles]) + + # save/replace ban_expiration to redis under "user:{userId}:baninfo" + ban_key = "user:#{db_user.id}:baninfo" + ban_info = %{} + if db_user.ban_expiration != nil do Map.add(ban_info, :expiration, db_user.ban_expiration) end + if db_user.malicious_score >= 1 do Map.add(ban_info, :malicious_score, db_user.malicious_score) end + Redix.command(:redix, ["DEL", ban_key]) + key_count = ban_info + |> Map.keys + |> List.length + unless key_count == 0 do Redix.command(:redix, ["HSET", ban_key, "baninfo", ban_info]) end end def update_roles do From 4ecf1cdcc80a6c0a0ee780520d1472a8e5dfb965 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Tue, 6 Sep 2022 15:20:00 -1000 Subject: [PATCH 009/231] feat(session.save): save/replace moderating boards to redis --- lib/epochtalk_server/session.ex | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index 5e3e77e5..f8a663e2 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -36,6 +36,13 @@ defmodule EpochtalkServer.Session do |> Map.keys |> List.length unless key_count == 0 do Redix.command(:redix, ["HSET", ban_key, "baninfo", ban_info]) end + + # save/replace moderating boards to redis under "user:{userId}:moderating" + moderating_key = "user:#{db_user.id}:moderating" + Redix.command(:redix, ["DEL", moderating_key]) + unless db_user.moderating == nil or db_user.moderating == [] do + Redix.command(:redix, ["SADD", moderating_key, db_user.moderating]) + end end def update_roles do From 79e04ef744b968891fd7c15967d99430916d627f Mon Sep 17 00:00:00 2001 From: unenglishable Date: Tue, 6 Sep 2022 15:21:43 -1000 Subject: [PATCH 010/231] refactor(session.save): use unless expiration nil --- lib/epochtalk_server/session.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index f8a663e2..671369b9 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -29,7 +29,7 @@ defmodule EpochtalkServer.Session do # save/replace ban_expiration to redis under "user:{userId}:baninfo" ban_key = "user:#{db_user.id}:baninfo" ban_info = %{} - if db_user.ban_expiration != nil do Map.add(ban_info, :expiration, db_user.ban_expiration) end + unless db_user.ban_expiration == nil do Map.add(ban_info, :expiration, db_user.ban_expiration) end if db_user.malicious_score >= 1 do Map.add(ban_info, :malicious_score, db_user.malicious_score) end Redix.command(:redix, ["DEL", ban_key]) key_count = ban_info From dd9397b5ff8fcc26126572e4f572c8aa4cc57afd Mon Sep 17 00:00:00 2001 From: unenglishable Date: Tue, 6 Sep 2022 15:39:44 -1000 Subject: [PATCH 011/231] docs(session.save): add todo's --- lib/epochtalk_server/session.ex | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index 671369b9..e9934107 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -3,6 +3,7 @@ defmodule EpochtalkServer.Session do # replace db_user role with role lookups # |> Map.put(:role, Enum.map(db_user.roles, &(&1.lookup)) # actually, just assume role lookups are there + # TODO: return role lookups from db instead of entire roles # set default role if necessary roles = db_user.roles @@ -43,6 +44,15 @@ defmodule EpochtalkServer.Session do unless db_user.moderating == nil or db_user.moderating == [] do Redix.command(:redix, ["SADD", moderating_key, db_user.moderating]) end + + # TODO: do this outside of here, need guardian token + # -- or don't do it if we don't need this functionality + # // save user-session to redis set under "user:{userId}:sessions" + # .then(function() { + # var userSessionKey = 'user:' + dbUser.id + ':sessions'; + # return redis.saddAsync(userSessionKey, decodedToken.sessionId); + # }) + # .then(function() { return formatUserReply(token, dbUser); }); end def update_roles do From 5d25fc109b449ce865a5cd170003e6a731cb90db Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 8 Sep 2022 12:13:08 -1000 Subject: [PATCH 012/231] feat(session): guard clause for update roles default case; empty list or nil, call update_roles with roles = ['user'] --- lib/epochtalk_server/session.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index e9934107..da2f4688 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -54,8 +54,8 @@ defmodule EpochtalkServer.Session do # }) # .then(function() { return formatUserReply(token, dbUser); }); end - def update_roles do - + def update_roles(user_id, roles) when roles == nil or roles == [] do + update_roles(user_id, [%{lookup: ['user']}]) end def update_moderating do From 713b47586bfaac2a297ce1e93c2962ac061706fd Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 8 Sep 2022 12:19:03 -1000 Subject: [PATCH 013/231] refactor(session.save): use guard clause for setting default role --- lib/epochtalk_server/session.ex | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index da2f4688..cdfaddeb 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -1,19 +1,14 @@ defmodule EpochtalkServer.Session do + # use default role + def save(db_user) when db_user.roles == nil or db_user.roles == [] do + db_user |> Map.put(:roles, ['user']) |> save() + end def save(db_user) do # replace db_user role with role lookups # |> Map.put(:role, Enum.map(db_user.roles, &(&1.lookup)) # actually, just assume role lookups are there # TODO: return role lookups from db instead of entire roles - # set default role if necessary - roles = db_user.roles - |> List.length - |> case do - 0 -> ['user'] - _ -> db_user.roles - end - db_user = Map.put(db_user, :roles, roles) - # save username, avatar to redis hash under "user:{userId}" user_key = "user:#{db_user.id}" case db_user.avatar do From 783290c644b818523aeb33813806cdde76b5f256 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 8 Sep 2022 12:20:37 -1000 Subject: [PATCH 014/231] docs(session.update_roles): comment for default role --- lib/epochtalk_server/session.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index cdfaddeb..9f64ef7a 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -49,6 +49,7 @@ defmodule EpochtalkServer.Session do # }) # .then(function() { return formatUserReply(token, dbUser); }); end + # use default role def update_roles(user_id, roles) when roles == nil or roles == [] do update_roles(user_id, [%{lookup: ['user']}]) end From c7124144e0a44d419dc81dd9f9ceaef6d5dec3c7 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 8 Sep 2022 13:38:40 -1000 Subject: [PATCH 015/231] feat(session): implement save_roles --- lib/epochtalk_server/session.ex | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index 9f64ef7a..41df8db8 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -1,4 +1,5 @@ defmodule EpochtalkServer.Session do + @role_key "user:#{user_id}:roles" # use default role def save(db_user) when db_user.roles == nil or db_user.roles == [] do db_user |> Map.put(:roles, ['user']) |> save() @@ -62,4 +63,9 @@ defmodule EpochtalkServer.Session do def update_ban_info do end + # save/replace roles to redis under "user:{userId}:roles" + defp save_roles(user_id, roles) do + Redix.command(:redix, ["DEL", @role_key]) + Redix.command(:redix, ["SADD", @role_key, roles]) + end end From fcce31022b2890c2f1b4d630e4b6f5dc89a0accf Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 8 Sep 2022 13:42:05 -1000 Subject: [PATCH 016/231] refactor(session.save): use save_roles --- lib/epochtalk_server/session.ex | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index 41df8db8..c3b08a28 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -18,10 +18,7 @@ defmodule EpochtalkServer.Session do _ -> Redix.command(:redix, ["HSET", user_key, "username", db_user.username, "avatar", db_user.avatar]) end - # save/replace roles to redis under "user:{userId}:roles" - role_key = "user:#{db_user.id}:roles" - Redix.command(:redix, ["DEL", role_key]) - Redix.command(:redix, ["SADD", role_key, db_user.roles]) + save_roles(db_user.id, db_user.roles) # save/replace ban_expiration to redis under "user:{userId}:baninfo" ban_key = "user:#{db_user.id}:baninfo" From 5f0266cffac112d8449ec62781a09db911f73b3b Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 8 Sep 2022 13:45:00 -1000 Subject: [PATCH 017/231] docs(session.save): remove commented code --- lib/epochtalk_server/session.ex | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index c3b08a28..60225c9f 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -5,9 +5,6 @@ defmodule EpochtalkServer.Session do db_user |> Map.put(:roles, ['user']) |> save() end def save(db_user) do - # replace db_user role with role lookups - # |> Map.put(:role, Enum.map(db_user.roles, &(&1.lookup)) - # actually, just assume role lookups are there # TODO: return role lookups from db instead of entire roles # save username, avatar to redis hash under "user:{userId}" From 1ca98a993f604ee647cbdfbc069b1c59c93fdff6 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 8 Sep 2022 13:45:27 -1000 Subject: [PATCH 018/231] refactor(session.update): assume role_lookups as argument --- lib/epochtalk_server/session.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index 60225c9f..126cd1f4 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -45,8 +45,8 @@ defmodule EpochtalkServer.Session do # .then(function() { return formatUserReply(token, dbUser); }); end # use default role - def update_roles(user_id, roles) when roles == nil or roles == [] do - update_roles(user_id, [%{lookup: ['user']}]) + def update_roles(user_id, role_lookups) when role_lookups == nil or role_lookups == [] do + update_roles(user_id, ['user']) end def update_moderating do From ac087d8c7e476077b80f84902b6f582707332e50 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 8 Sep 2022 13:45:52 -1000 Subject: [PATCH 019/231] feat(session.update): implement update_roles --- lib/epochtalk_server/session.ex | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index 126cd1f4..7b24a874 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -48,6 +48,9 @@ defmodule EpochtalkServer.Session do def update_roles(user_id, role_lookups) when role_lookups == nil or role_lookups == [] do update_roles(user_id, ['user']) end + def update_roles(user_id, [role_lookups]) do + save_roles(user_id, role_lookups) + end def update_moderating do end From 91b3731d6e11eaf0ae86f0e27d10a659da6812c4 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 8 Sep 2022 13:47:19 -1000 Subject: [PATCH 020/231] fix(session.update): correct guard [role_lookups] -> is_list --- lib/epochtalk_server/session.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index 7b24a874..b7764fb3 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -48,7 +48,7 @@ defmodule EpochtalkServer.Session do def update_roles(user_id, role_lookups) when role_lookups == nil or role_lookups == [] do update_roles(user_id, ['user']) end - def update_roles(user_id, [role_lookups]) do + def update_roles(user_id, role_lookups) when is_list(role_lookups) do save_roles(user_id, role_lookups) end def update_moderating do From b5efd454076a733a4055fb9b8204c90fa089067b Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 9 Sep 2022 11:48:04 -1000 Subject: [PATCH 021/231] refactor(session.save): consolidate update_roles --- lib/epochtalk_server/session.ex | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index b7764fb3..b4c4eac7 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -1,9 +1,4 @@ defmodule EpochtalkServer.Session do - @role_key "user:#{user_id}:roles" - # use default role - def save(db_user) when db_user.roles == nil or db_user.roles == [] do - db_user |> Map.put(:roles, ['user']) |> save() - end def save(db_user) do # TODO: return role lookups from db instead of entire roles @@ -15,7 +10,7 @@ defmodule EpochtalkServer.Session do _ -> Redix.command(:redix, ["HSET", user_key, "username", db_user.username, "avatar", db_user.avatar]) end - save_roles(db_user.id, db_user.roles) + update_roles(db_user.id, db_user.roles) # save/replace ban_expiration to redis under "user:{userId}:baninfo" ban_key = "user:#{db_user.id}:baninfo" @@ -45,11 +40,12 @@ defmodule EpochtalkServer.Session do # .then(function() { return formatUserReply(token, dbUser); }); end # use default role - def update_roles(user_id, role_lookups) when role_lookups == nil or role_lookups == [] do - update_roles(user_id, ['user']) - end - def update_roles(user_id, role_lookups) when is_list(role_lookups) do - save_roles(user_id, role_lookups) + def update_roles(user_id, roles) when is_list(roles) do + role_lookups = roles + |> Enum.map(&(&1.lookup)) + key = role_key(user_id) + Redix.command(:redix, ["DEL", key]) + Redix.command(:redix, ["SADD", key, role_lookups]) end def update_moderating do @@ -61,8 +57,5 @@ defmodule EpochtalkServer.Session do end # save/replace roles to redis under "user:{userId}:roles" - defp save_roles(user_id, roles) do - Redix.command(:redix, ["DEL", @role_key]) - Redix.command(:redix, ["SADD", @role_key, roles]) - end + defp role_key(user_id), do: "user:#{user_id}:roles" end From 318bd90cc239df2fbccbcb3af10864a864c2865a Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 9 Sep 2022 11:56:39 -1000 Subject: [PATCH 022/231] refactor(session): implement generate_key and use generically --- lib/epochtalk_server/session.ex | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index b4c4eac7..5d4b3428 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -3,7 +3,7 @@ defmodule EpochtalkServer.Session do # TODO: return role lookups from db instead of entire roles # save username, avatar to redis hash under "user:{userId}" - user_key = "user:#{db_user.id}" + user_key = generate_key(db_user.id, "user") case db_user.avatar do "" -> Redix.command(:redix, ["HSET", user_key, "username", db_user.username]) # set avatar if it's available @@ -13,7 +13,7 @@ defmodule EpochtalkServer.Session do update_roles(db_user.id, db_user.roles) # save/replace ban_expiration to redis under "user:{userId}:baninfo" - ban_key = "user:#{db_user.id}:baninfo" + ban_key = generate_key(db_user.id, "baninfo") ban_info = %{} unless db_user.ban_expiration == nil do Map.add(ban_info, :expiration, db_user.ban_expiration) end if db_user.malicious_score >= 1 do Map.add(ban_info, :malicious_score, db_user.malicious_score) end @@ -24,7 +24,7 @@ defmodule EpochtalkServer.Session do unless key_count == 0 do Redix.command(:redix, ["HSET", ban_key, "baninfo", ban_info]) end # save/replace moderating boards to redis under "user:{userId}:moderating" - moderating_key = "user:#{db_user.id}:moderating" + moderating_key = generate_key(db_user.id, "moderating") Redix.command(:redix, ["DEL", moderating_key]) unless db_user.moderating == nil or db_user.moderating == [] do Redix.command(:redix, ["SADD", moderating_key, db_user.moderating]) @@ -43,9 +43,9 @@ defmodule EpochtalkServer.Session do def update_roles(user_id, roles) when is_list(roles) do role_lookups = roles |> Enum.map(&(&1.lookup)) - key = role_key(user_id) - Redix.command(:redix, ["DEL", key]) - Redix.command(:redix, ["SADD", key, role_lookups]) + role_key = generate_key(user_id, "roles") + Redix.command(:redix, ["DEL", role_key]) + Redix.command(:redix, ["SADD", role_key, role_lookups]) end def update_moderating do @@ -57,5 +57,6 @@ defmodule EpochtalkServer.Session do end # save/replace roles to redis under "user:{userId}:roles" - defp role_key(user_id), do: "user:#{user_id}:roles" + defp generate_key(user_id, "user"), do: "user:#{user_id}" + defp generate_key(user_id, type), do: "user:#{user_id}:#{type}" end From 6d068fab9cf87786132e0999e831304948af4cfe Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 9 Sep 2022 12:01:14 -1000 Subject: [PATCH 023/231] docs(session): move comment --- lib/epochtalk_server/session.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index 5d4b3428..59d5401b 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -41,6 +41,7 @@ defmodule EpochtalkServer.Session do end # use default role def update_roles(user_id, roles) when is_list(roles) do + # save/replace roles to redis under "user:{userId}:roles" role_lookups = roles |> Enum.map(&(&1.lookup)) role_key = generate_key(user_id, "roles") @@ -56,7 +57,6 @@ defmodule EpochtalkServer.Session do def update_ban_info do end - # save/replace roles to redis under "user:{userId}:roles" defp generate_key(user_id, "user"), do: "user:#{user_id}" defp generate_key(user_id, type), do: "user:#{user_id}:#{type}" end From 976705a1e415097d2085bfd43edf04da8069970c Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 9 Sep 2022 12:01:48 -1000 Subject: [PATCH 024/231] feat(session.update_moderating): implement and refactor --- lib/epochtalk_server/session.ex | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index 59d5401b..678c40a4 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -23,13 +23,7 @@ defmodule EpochtalkServer.Session do |> List.length unless key_count == 0 do Redix.command(:redix, ["HSET", ban_key, "baninfo", ban_info]) end - # save/replace moderating boards to redis under "user:{userId}:moderating" - moderating_key = generate_key(db_user.id, "moderating") - Redix.command(:redix, ["DEL", moderating_key]) - unless db_user.moderating == nil or db_user.moderating == [] do - Redix.command(:redix, ["SADD", moderating_key, db_user.moderating]) - end - + update_moderating(db_user.id, db_user.moderating) # TODO: do this outside of here, need guardian token # -- or don't do it if we don't need this functionality # // save user-session to redis set under "user:{userId}:sessions" @@ -48,8 +42,13 @@ defmodule EpochtalkServer.Session do Redix.command(:redix, ["DEL", role_key]) Redix.command(:redix, ["SADD", role_key, role_lookups]) end - def update_moderating do - + def update_moderating(user_id, moderating) do + # save/replace moderating boards to redis under "user:{userId}:moderating" + moderating_key = generate_key(user_id, "moderating") + Redix.command(:redix, ["DEL", moderating_key]) + unless moderating == nil or moderating == [] do + Redix.command(:redix, ["SADD", moderating_key, moderating]) + end end def update_user_info do From 8a0890425b66dbff520186eecca9b7d2f1af6919 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 9 Sep 2022 12:24:52 -1000 Subject: [PATCH 025/231] docs(session): userId -> user_id in comments --- lib/epochtalk_server/session.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index 678c40a4..f0f216f1 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -12,7 +12,7 @@ defmodule EpochtalkServer.Session do update_roles(db_user.id, db_user.roles) - # save/replace ban_expiration to redis under "user:{userId}:baninfo" + # save/replace ban_expiration to redis under "user:{user_id}:baninfo" ban_key = generate_key(db_user.id, "baninfo") ban_info = %{} unless db_user.ban_expiration == nil do Map.add(ban_info, :expiration, db_user.ban_expiration) end @@ -26,7 +26,7 @@ defmodule EpochtalkServer.Session do update_moderating(db_user.id, db_user.moderating) # TODO: do this outside of here, need guardian token # -- or don't do it if we don't need this functionality - # // save user-session to redis set under "user:{userId}:sessions" + # // save user-session to redis set under "user:{user_id}:sessions" # .then(function() { # var userSessionKey = 'user:' + dbUser.id + ':sessions'; # return redis.saddAsync(userSessionKey, decodedToken.sessionId); @@ -35,7 +35,7 @@ defmodule EpochtalkServer.Session do end # use default role def update_roles(user_id, roles) when is_list(roles) do - # save/replace roles to redis under "user:{userId}:roles" + # save/replace roles to redis under "user:{user_id}:roles" role_lookups = roles |> Enum.map(&(&1.lookup)) role_key = generate_key(user_id, "roles") From 8a39bb1a242f13238cd36998d43a7124c69b120b Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 9 Sep 2022 12:25:10 -1000 Subject: [PATCH 026/231] feat(session): implement update_user_info overloaded for missing avatar --- lib/epochtalk_server/session.ex | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index f0f216f1..9d7e24cb 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -1,15 +1,7 @@ defmodule EpochtalkServer.Session do def save(db_user) do # TODO: return role lookups from db instead of entire roles - - # save username, avatar to redis hash under "user:{userId}" - user_key = generate_key(db_user.id, "user") - case db_user.avatar do - "" -> Redix.command(:redix, ["HSET", user_key, "username", db_user.username]) - # set avatar if it's available - _ -> Redix.command(:redix, ["HSET", user_key, "username", db_user.username, "avatar", db_user.avatar]) - end - + update_user_info(db_user.id, db_user.username, db_user.avatar) update_roles(db_user.id, db_user.roles) # save/replace ban_expiration to redis under "user:{user_id}:baninfo" @@ -43,15 +35,27 @@ defmodule EpochtalkServer.Session do Redix.command(:redix, ["SADD", role_key, role_lookups]) end def update_moderating(user_id, moderating) do - # save/replace moderating boards to redis under "user:{userId}:moderating" + # save/replace moderating boards to redis under "user:{user_id}:moderating" moderating_key = generate_key(user_id, "moderating") Redix.command(:redix, ["DEL", moderating_key]) unless moderating == nil or moderating == [] do Redix.command(:redix, ["SADD", moderating_key, moderating]) end end - def update_user_info do - + def update_user_info(user_id, username) do + user_key = generate_key(db_user.id, "user") + # delete avatar from redis hash under "user:{user_id}" + Redis.command(:redis, ["HDEL", user_key, "avatar"]) + # save username to redis hash under "user:{user_id}" + Redix.command(:redix, ["HSET", user_key, "username", db_user.username]) + end + def update_user_info(user_id, username, avatar) when is_nil(avatar) or avatar == "" do + update_user_info(user_id, username) + end + def update_user_info(user_id, username, avatar) do + # save username, avatar to redis hash under "user:{user_id}" + user_key = generate_key(db_user.id, "user") + Redix.command(:redix, ["HSET", user_key, "username", db_user.username, "avatar", db_user.avatar]) end def update_ban_info do From fcee45931eaffe07982826524384f40bcf26f695 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 9 Sep 2022 13:16:14 -1000 Subject: [PATCH 027/231] fix(session): db_user.id -> user_id --- lib/epochtalk_server/session.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index 9d7e24cb..d58cf569 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -43,7 +43,7 @@ defmodule EpochtalkServer.Session do end end def update_user_info(user_id, username) do - user_key = generate_key(db_user.id, "user") + user_key = generate_key(user_id, "user") # delete avatar from redis hash under "user:{user_id}" Redis.command(:redis, ["HDEL", user_key, "avatar"]) # save username to redis hash under "user:{user_id}" @@ -54,7 +54,7 @@ defmodule EpochtalkServer.Session do end def update_user_info(user_id, username, avatar) do # save username, avatar to redis hash under "user:{user_id}" - user_key = generate_key(db_user.id, "user") + user_key = generate_key(user_id, "user") Redix.command(:redix, ["HSET", user_key, "username", db_user.username, "avatar", db_user.avatar]) end def update_ban_info do From 37a2eb30427e24c3ed0b3b7fcee98eff1df047b2 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 9 Sep 2022 13:18:43 -1000 Subject: [PATCH 028/231] feat(session): implement update_ban_info --- lib/epochtalk_server/session.ex | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index d58cf569..e455f7ed 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -3,18 +3,9 @@ defmodule EpochtalkServer.Session do # TODO: return role lookups from db instead of entire roles update_user_info(db_user.id, db_user.username, db_user.avatar) update_roles(db_user.id, db_user.roles) - - # save/replace ban_expiration to redis under "user:{user_id}:baninfo" - ban_key = generate_key(db_user.id, "baninfo") - ban_info = %{} - unless db_user.ban_expiration == nil do Map.add(ban_info, :expiration, db_user.ban_expiration) end - if db_user.malicious_score >= 1 do Map.add(ban_info, :malicious_score, db_user.malicious_score) end - Redix.command(:redix, ["DEL", ban_key]) - key_count = ban_info - |> Map.keys - |> List.length - unless key_count == 0 do Redix.command(:redix, ["HSET", ban_key, "baninfo", ban_info]) end - + ban_info = if Map.has_key?(db_user, :ban_expiration), do: %{ ban_expiration: db_user.ban_expiration }, else: %{} + ban_info = if Map.has_key?(db_user, :malicious_score), do: Map.put(ban_info, :malicious_score, db_user.malicious_score) + update_ban_info(db_user.id, ban_info) update_moderating(db_user.id, db_user.moderating) # TODO: do this outside of here, need guardian token # -- or don't do it if we don't need this functionality @@ -57,8 +48,11 @@ defmodule EpochtalkServer.Session do user_key = generate_key(user_id, "user") Redix.command(:redix, ["HSET", user_key, "username", db_user.username, "avatar", db_user.avatar]) end - def update_ban_info do - + def update_ban_info(user_id, ban_info) do + # save/replace ban_expiration to redis under "user:{user_id}:baninfo" + ban_key = generate_key(user_id, "baninfo") + Redix.command(:redix, ["HDEL", ban_key, "ban_expiration", "malicious_score"]) + Redix.command(:redix, ["HSET", ban_key, ban_info) end defp generate_key(user_id, "user"), do: "user:#{user_id}" defp generate_key(user_id, type), do: "user:#{user_id}:#{type}" From ce8fb08ab0d9736fca42de9a47820a273d77eaf8 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 12 Sep 2022 11:44:47 -1000 Subject: [PATCH 029/231] refactor(boards/cats): port board_mapping, refactor related files --- lib/epochtalk_server/models/board.ex | 4 +++- lib/epochtalk_server/models/board_moderators.ex | 6 ++++-- lib/epochtalk_server/models/category.ex | 4 +++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/epochtalk_server/models/board.ex b/lib/epochtalk_server/models/board.ex index 075fa237..bdd92138 100644 --- a/lib/epochtalk_server/models/board.ex +++ b/lib/epochtalk_server/models/board.ex @@ -2,6 +2,7 @@ defmodule EpochtalkServer.Models.Board do use Ecto.Schema import Ecto.Changeset alias EpochtalkServer.Models.Category + alias EpochtalkServer.Models.BoardMapping schema "boards" do field :name, :string @@ -15,7 +16,7 @@ defmodule EpochtalkServer.Models.Board do field :imported_at, :naive_datetime field :updated_at, :naive_datetime field :meta, :map - many_to_many :categories, Category, join_through: "board_mapping" + many_to_many :categories, Category, join_through: BoardMapping end def changeset(board, attrs) do @@ -25,4 +26,5 @@ defmodule EpochtalkServer.Models.Board do |> unique_constraint(:id, name: :boards_pkey) |> unique_constraint(:slug, name: :boards_slug_index) end + def insert(%Board{} = board), do: Repo.insert(board) end diff --git a/lib/epochtalk_server/models/board_moderators.ex b/lib/epochtalk_server/models/board_moderators.ex index 3cd06059..0a0cc95b 100644 --- a/lib/epochtalk_server/models/board_moderators.ex +++ b/lib/epochtalk_server/models/board_moderators.ex @@ -1,13 +1,15 @@ defmodule EpochtalkServer.Models.BoardModerators do use Ecto.Schema import Ecto.Changeset + alias EpochtalkServer.Repo alias EpochtalkServer.Models.User alias EpochtalkServer.Models.Board + alias EpochtalkServer.Models.BoardModerators @primary_key false schema "board_moderators" do - belongs_to :user_id, User - belongs_to :board_id, Board + belongs_to :user, User + belongs_to :board, Board end def changeset(permission, attrs \\ %{}) do diff --git a/lib/epochtalk_server/models/category.ex b/lib/epochtalk_server/models/category.ex index c908c78f..db98b77c 100644 --- a/lib/epochtalk_server/models/category.ex +++ b/lib/epochtalk_server/models/category.ex @@ -2,6 +2,7 @@ defmodule EpochtalkServer.Models.Category do use Ecto.Schema import Ecto.Changeset alias EpochtalkServer.Models.Board + alias EpochtalkServer.Models.BoardMapping schema "categories" do field :name, :string @@ -12,7 +13,7 @@ defmodule EpochtalkServer.Models.Category do field :imported_at, :naive_datetime field :updated_at, :naive_datetime field :meta, :map - many_to_many :boards, Board, join_through: "board_mapping" + many_to_many :boards, Board, join_through: BoardMapping end def changeset(board, attrs) do @@ -20,4 +21,5 @@ defmodule EpochtalkServer.Models.Category do |> cast(attrs, [:id, :name, :view_order, :viewable_by, :postable_by, :created_at, :imported_at, :updated_at, :meta]) |> unique_constraint(:id, name: :categories_pkey) end + def insert(%Category{} = category), do: Repo.insert(category) end From 9b8f0dc53a4f921a14b03d0f646e5674dd7295a0 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 12 Sep 2022 12:00:16 -1000 Subject: [PATCH 030/231] fix(role-user): primary_key false no id for role-user table --- lib/epochtalk_server/models/role_user.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/epochtalk_server/models/role_user.ex b/lib/epochtalk_server/models/role_user.ex index 7729fd7f..548f3a1c 100644 --- a/lib/epochtalk_server/models/role_user.ex +++ b/lib/epochtalk_server/models/role_user.ex @@ -4,6 +4,7 @@ defmodule EpochtalkServer.Models.RoleUser do alias EpochtalkServer.Models.User alias EpochtalkServer.Models.Role + @primary_key false schema "roles_users" do belongs_to :user, User belongs_to :role, Role From ecb36af189f66e7808525777651d51b8398288a5 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 12 Sep 2022 12:00:53 -1000 Subject: [PATCH 031/231] refactor(role-user): fix changeset permission -> role_user copied code --- lib/epochtalk_server/models/role_user.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server/models/role_user.ex b/lib/epochtalk_server/models/role_user.ex index 548f3a1c..9fec2528 100644 --- a/lib/epochtalk_server/models/role_user.ex +++ b/lib/epochtalk_server/models/role_user.ex @@ -10,8 +10,8 @@ defmodule EpochtalkServer.Models.RoleUser do belongs_to :role, Role end - def changeset(permission, attrs \\ %{}) do - permission + def changeset(role_user, attrs \\ %{}) do + role_user |> cast(attrs, [:user_id, :role_id]) |> validate_required([:user_id, :role_id]) end From 376cbf60d1f2a9bab36909f11b7b7404e2e8dd40 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 12 Sep 2022 12:02:08 -1000 Subject: [PATCH 032/231] feat(seed-user): add module for seeding user will refactor this to move parts out into the rest of the repo, where code is supposed to be --- lib/epochtalk_server/seed/user.ex | 38 +++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 lib/epochtalk_server/seed/user.ex diff --git a/lib/epochtalk_server/seed/user.ex b/lib/epochtalk_server/seed/user.ex new file mode 100644 index 00000000..5345efe9 --- /dev/null +++ b/lib/epochtalk_server/seed/user.ex @@ -0,0 +1,38 @@ +defmodule EpochtalkServer.Seed.User do + alias EpochtalkServer.Models.User + alias EpochtalkServer.Models.RoleUser + alias EpochtalkServer.Repo + + def process_args([username, email, password, "admin" = admin]) do + case process_args([username, email, password]) do + {:ok, user} -> + %RoleUser{} + |>RoleUser.changeset(%{user_id: user.id, role_id: 1}) + |> Repo.insert + |> case do + {:ok, role_user} -> + IO.puts("Successfully seeded admin role") + IO.inspect(role_user) + end + end + end + def process_args([username, email, password]) do + attrs = %{username: username, email: email, password: password} + %User{} + |> User.registration_changeset(attrs) + |> Repo.insert + |> case do + {:ok, user} -> + IO.puts("Successfully seeded user") + IO.inspect(user) + {:ok, user} + {:error, changeset} -> + IO.puts("Error seeding user") + IO.inspect(changeset) + {:error, changeset} + _ -> + IO.puts("Unknown issue seeding user") + IO.inspect(attrs) + end + end +end From b6fe615912186a157c2c250c25bcb2e845a24dff Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 12 Sep 2022 12:02:44 -1000 Subject: [PATCH 033/231] feat(seed-user): add user seed script uses user seed module --- priv/repo/seed_user.exs | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 priv/repo/seed_user.exs diff --git a/priv/repo/seed_user.exs b/priv/repo/seed_user.exs new file mode 100644 index 00000000..65a141be --- /dev/null +++ b/priv/repo/seed_user.exs @@ -0,0 +1,3 @@ +alias EpochtalkServer.Seed.User +System.argv() +|> User.process_args From b87d5f289479632471456d29a6cb9a9050d45ca3 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 12 Sep 2022 12:04:24 -1000 Subject: [PATCH 034/231] refactor(mix): alphabetize mix --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 31d1e3f5..84277da5 100644 --- a/mix.exs +++ b/mix.exs @@ -66,8 +66,8 @@ defmodule EpochtalkServer.MixProject do "db.migrate": ["ecto.migrate", "ecto.dump"], "db.rollback": ["ecto.rollback", "ecto.dump"], "seed.all": ["seed.prp", "seed.permissions", "seed.roles", "seed.rp"], - "seed.prp": ["run priv/repo/process_roles_permissions.exs"], "seed.permissions": ["run priv/repo/seed_permissions.exs"], + "seed.prp": ["run priv/repo/process_roles_permissions.exs"], "seed.roles": ["run priv/repo/seed_roles.exs"], "seed.rp": ["run priv/repo/seed_roles_permissions.exs"], test: ["ecto.create --quiet", "ecto.migrate", "test"] From 4906c1321fbad6812a463a09f157f99e35be2cfb Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 12 Sep 2022 12:05:03 -1000 Subject: [PATCH 035/231] feat(mix): add seed user script to mix --- mix.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/mix.exs b/mix.exs index 84277da5..059f2238 100644 --- a/mix.exs +++ b/mix.exs @@ -70,6 +70,7 @@ defmodule EpochtalkServer.MixProject do "seed.prp": ["run priv/repo/process_roles_permissions.exs"], "seed.roles": ["run priv/repo/seed_roles.exs"], "seed.rp": ["run priv/repo/seed_roles_permissions.exs"], + "seed.user": ["run priv/repo/seed_user.exs"], test: ["ecto.create --quiet", "ecto.migrate", "test"] ] end From 69b2d21c629f4198e2fe2a04f7a40106504bb64b Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 12 Sep 2022 12:27:43 -1000 Subject: [PATCH 036/231] feat(role-user): implement set_admin and set_role set_admin calls set_role with @admin_role_id as role_id --- lib/epochtalk_server/models/role_user.ex | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/epochtalk_server/models/role_user.ex b/lib/epochtalk_server/models/role_user.ex index 9fec2528..a5ed277a 100644 --- a/lib/epochtalk_server/models/role_user.ex +++ b/lib/epochtalk_server/models/role_user.ex @@ -3,6 +3,7 @@ defmodule EpochtalkServer.Models.RoleUser do import Ecto.Changeset alias EpochtalkServer.Models.User alias EpochtalkServer.Models.Role + @admin_role_id 1 @primary_key false schema "roles_users" do @@ -15,4 +16,10 @@ defmodule EpochtalkServer.Models.RoleUser do |> cast(attrs, [:user_id, :role_id]) |> validate_required([:user_id, :role_id]) end + def set_admin(%User{} = user), do: set_role(user, @admin_role_id) + def set_role(%User{} = user, role_id) do + %RoleUser{} + |> RoleUser.changeset(%{user_id: user.id, role_id: role_id}) + |> Repo.insert + end end From a1ce80963acc19e601653afa980605aa95598448 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 12 Sep 2022 12:33:00 -1000 Subject: [PATCH 037/231] feat(user): implement create_user with overload on admin = true using transaction in admin create to roll back if role user insert fails --- lib/epochtalk_server/models/user.ex | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/epochtalk_server/models/user.ex b/lib/epochtalk_server/models/user.ex index a5db87cd..68e531b3 100644 --- a/lib/epochtalk_server/models/user.ex +++ b/lib/epochtalk_server/models/user.ex @@ -7,6 +7,7 @@ defmodule EpochtalkServer.Models.User do alias EpochtalkServer.Models.Profile alias EpochtalkServer.Models.Preference alias EpochtalkServer.Models.Role + alias EpochtalkServer.Models.RoleUser schema "users" do field :email, :string @@ -35,6 +36,23 @@ defmodule EpochtalkServer.Models.User do |> validate_email() |> validate_password() end + # create admin, for seeding + def create_user(user_atrs, true = _admin) do + Repo.transaction(fn -> + create_user(user_atrs) + |> case do + {:ok, user} -> + user + |> RoleUser.set_admin + end + end) + end + # create user, for seeding + def create_user(user_atrs) do + %User{} + |> User.registration_changeset(user_attrs) + |> Repo.insert + end def with_username_exists?(username), do: Repo.exists?(from u in User, where: u.username == ^username) def with_email_exists?(email), do: Repo.exists?(from u in User, where: u.email == ^email) From fd0683961fe971c06e8092a63959a3043ce1c4a3 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 12 Sep 2022 12:34:48 -1000 Subject: [PATCH 038/231] refactor(seed-user): use code in User/RoleUser to seed user with role removed old lib/epochtalk_server/seed/user.ex since functionality is covered in the models now fix function overloading on System.argv by checking with case --- lib/epochtalk_server/seed/user.ex | 38 ------------------------------- priv/repo/seed_user.exs | 13 +++++++++-- 2 files changed, 11 insertions(+), 40 deletions(-) delete mode 100644 lib/epochtalk_server/seed/user.ex diff --git a/lib/epochtalk_server/seed/user.ex b/lib/epochtalk_server/seed/user.ex deleted file mode 100644 index 5345efe9..00000000 --- a/lib/epochtalk_server/seed/user.ex +++ /dev/null @@ -1,38 +0,0 @@ -defmodule EpochtalkServer.Seed.User do - alias EpochtalkServer.Models.User - alias EpochtalkServer.Models.RoleUser - alias EpochtalkServer.Repo - - def process_args([username, email, password, "admin" = admin]) do - case process_args([username, email, password]) do - {:ok, user} -> - %RoleUser{} - |>RoleUser.changeset(%{user_id: user.id, role_id: 1}) - |> Repo.insert - |> case do - {:ok, role_user} -> - IO.puts("Successfully seeded admin role") - IO.inspect(role_user) - end - end - end - def process_args([username, email, password]) do - attrs = %{username: username, email: email, password: password} - %User{} - |> User.registration_changeset(attrs) - |> Repo.insert - |> case do - {:ok, user} -> - IO.puts("Successfully seeded user") - IO.inspect(user) - {:ok, user} - {:error, changeset} -> - IO.puts("Error seeding user") - IO.inspect(changeset) - {:error, changeset} - _ -> - IO.puts("Unknown issue seeding user") - IO.inspect(attrs) - end - end -end diff --git a/priv/repo/seed_user.exs b/priv/repo/seed_user.exs index 65a141be..9f76ad26 100644 --- a/priv/repo/seed_user.exs +++ b/priv/repo/seed_user.exs @@ -1,3 +1,12 @@ -alias EpochtalkServer.Seed.User +alias EpochtalkServer.Models.User System.argv() -|> User.process_args +|> case do + [username, email, password, "admin" = _admin] -> + admin = true + User.seed_user(username, email, password, admin) + [username, email, password] -> + User.seed_user(username, email, password) +end +|> case do + {:ok, _} -> IO.puts("Successfully seeded user") + {:error, _} -> IO.puts("Error seeding user") From 10344613fd9721fd59ffb29f2417dfed657f7743 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 12 Sep 2022 12:37:21 -1000 Subject: [PATCH 039/231] fix(user): user_atrs -> user_attrs --- lib/epochtalk_server/models/user.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/epochtalk_server/models/user.ex b/lib/epochtalk_server/models/user.ex index 68e531b3..a810fdfb 100644 --- a/lib/epochtalk_server/models/user.ex +++ b/lib/epochtalk_server/models/user.ex @@ -37,9 +37,9 @@ defmodule EpochtalkServer.Models.User do |> validate_password() end # create admin, for seeding - def create_user(user_atrs, true = _admin) do + def create_user(user_attrs, true = _admin) do Repo.transaction(fn -> - create_user(user_atrs) + create_user(user_attrs) |> case do {:ok, user} -> user @@ -48,7 +48,7 @@ defmodule EpochtalkServer.Models.User do end) end # create user, for seeding - def create_user(user_atrs) do + def create_user(user_attrs) do %User{} |> User.registration_changeset(user_attrs) |> Repo.insert From e66ef40b3fd46d64859f427fb66a14559388eb6a Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 12 Sep 2022 12:39:52 -1000 Subject: [PATCH 040/231] fix(board): add alias for Board --- lib/epochtalk_server/models/board.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/epochtalk_server/models/board.ex b/lib/epochtalk_server/models/board.ex index bdd92138..1b564654 100644 --- a/lib/epochtalk_server/models/board.ex +++ b/lib/epochtalk_server/models/board.ex @@ -3,6 +3,7 @@ defmodule EpochtalkServer.Models.Board do import Ecto.Changeset alias EpochtalkServer.Models.Category alias EpochtalkServer.Models.BoardMapping + alias EpochtalkServer.Models.Board schema "boards" do field :name, :string From c99d70aa06aecfcab2e77dc845cc035928df8f29 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 12 Sep 2022 12:41:06 -1000 Subject: [PATCH 041/231] fix(category): add alias for category --- lib/epochtalk_server/models/category.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/epochtalk_server/models/category.ex b/lib/epochtalk_server/models/category.ex index db98b77c..f704ff52 100644 --- a/lib/epochtalk_server/models/category.ex +++ b/lib/epochtalk_server/models/category.ex @@ -2,6 +2,7 @@ defmodule EpochtalkServer.Models.Category do use Ecto.Schema import Ecto.Changeset alias EpochtalkServer.Models.Board + alias EpochtalkServer.Models.Category alias EpochtalkServer.Models.BoardMapping schema "categories" do From f372084dc33f6b8debaaf14dfc1048996da19813 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 12 Sep 2022 12:43:29 -1000 Subject: [PATCH 042/231] fix(role-user): add alias for role-user --- lib/epochtalk_server/models/role_user.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/epochtalk_server/models/role_user.ex b/lib/epochtalk_server/models/role_user.ex index a5ed277a..eb89c004 100644 --- a/lib/epochtalk_server/models/role_user.ex +++ b/lib/epochtalk_server/models/role_user.ex @@ -3,6 +3,7 @@ defmodule EpochtalkServer.Models.RoleUser do import Ecto.Changeset alias EpochtalkServer.Models.User alias EpochtalkServer.Models.Role + alias EpochtalkServer.Models.RoleUser @admin_role_id 1 @primary_key false From 913590c27ddeded1546e2c88c2db2b156338e206 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 12 Sep 2022 13:28:11 -1000 Subject: [PATCH 043/231] fix(role-user): alias Repo --- lib/epochtalk_server/models/role_user.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/epochtalk_server/models/role_user.ex b/lib/epochtalk_server/models/role_user.ex index eb89c004..4f22e59b 100644 --- a/lib/epochtalk_server/models/role_user.ex +++ b/lib/epochtalk_server/models/role_user.ex @@ -1,6 +1,7 @@ defmodule EpochtalkServer.Models.RoleUser do use Ecto.Schema import Ecto.Changeset + alias EpochtalkServer.Repo alias EpochtalkServer.Models.User alias EpochtalkServer.Models.Role alias EpochtalkServer.Models.RoleUser From 9033421f6c801334146bd7af25489ce8721a0361 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 12 Sep 2022 13:28:29 -1000 Subject: [PATCH 044/231] fix(seed-user): User.seed_user -> User.create_user --- priv/repo/seed_user.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/priv/repo/seed_user.exs b/priv/repo/seed_user.exs index 9f76ad26..95c06518 100644 --- a/priv/repo/seed_user.exs +++ b/priv/repo/seed_user.exs @@ -3,9 +3,9 @@ System.argv() |> case do [username, email, password, "admin" = _admin] -> admin = true - User.seed_user(username, email, password, admin) + User.create_user(%{username: username, email: email, password: password}, admin) [username, email, password] -> - User.seed_user(username, email, password) + User.create_user(%{username: username, email: email, password: password}) end |> case do {:ok, _} -> IO.puts("Successfully seeded user") From 30260bfb53c0ad7242a832e972ace7cd63ccf8f5 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 12 Sep 2022 13:28:52 -1000 Subject: [PATCH 045/231] fix(seed-user): end for case --- priv/repo/seed_user.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/priv/repo/seed_user.exs b/priv/repo/seed_user.exs index 95c06518..c4d3e002 100644 --- a/priv/repo/seed_user.exs +++ b/priv/repo/seed_user.exs @@ -10,3 +10,4 @@ end |> case do {:ok, _} -> IO.puts("Successfully seeded user") {:error, _} -> IO.puts("Error seeding user") +end From fdb952c770144dd925c286d817bc40197992ccc2 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 12 Sep 2022 14:57:52 -1000 Subject: [PATCH 046/231] fix(board-model): add repo alias for boards model --- lib/epochtalk_server/models/board.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/epochtalk_server/models/board.ex b/lib/epochtalk_server/models/board.ex index 1b564654..79a79a49 100644 --- a/lib/epochtalk_server/models/board.ex +++ b/lib/epochtalk_server/models/board.ex @@ -1,6 +1,7 @@ defmodule EpochtalkServer.Models.Board do use Ecto.Schema import Ecto.Changeset + alias EpochtalkServer.Repo alias EpochtalkServer.Models.Category alias EpochtalkServer.Models.BoardMapping alias EpochtalkServer.Models.Board From 3bade9ad276942ad020ded580814ae85f38b1cc5 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 12 Sep 2022 14:58:51 -1000 Subject: [PATCH 047/231] feat(moderated-boards): add query to get user's moderated boards --- lib/epochtalk_server/models/board_moderators.ex | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/epochtalk_server/models/board_moderators.ex b/lib/epochtalk_server/models/board_moderators.ex index 0a0cc95b..748fc77c 100644 --- a/lib/epochtalk_server/models/board_moderators.ex +++ b/lib/epochtalk_server/models/board_moderators.ex @@ -17,4 +17,8 @@ defmodule EpochtalkServer.Models.BoardModerators do |> cast(attrs, [:user_id, :board_id]) |> validate_required([:user_id, :board_id]) end + + def get_boards(user_id) when is_integer(user_id) do + Repo.get_by(BoardModerators, user_id: user_id) + end end From abb392fe858f09ebf34c19bd73cd8060d2fae7d3 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 12 Sep 2022 14:59:13 -1000 Subject: [PATCH 048/231] fix(category-model): add repo alias --- lib/epochtalk_server/models/category.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/epochtalk_server/models/category.ex b/lib/epochtalk_server/models/category.ex index f704ff52..11268bff 100644 --- a/lib/epochtalk_server/models/category.ex +++ b/lib/epochtalk_server/models/category.ex @@ -1,6 +1,7 @@ defmodule EpochtalkServer.Models.Category do use Ecto.Schema import Ecto.Changeset + alias EpochtalkServer.Repo alias EpochtalkServer.Models.Board alias EpochtalkServer.Models.Category alias EpochtalkServer.Models.BoardMapping From 723b5b112282f23e15dccabba2b19799483a7f54 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 12 Sep 2022 14:59:49 -1000 Subject: [PATCH 049/231] feat(category-model): implement update_for_board_mapping --- lib/epochtalk_server/models/category.ex | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/epochtalk_server/models/category.ex b/lib/epochtalk_server/models/category.ex index 11268bff..ac2945a9 100644 --- a/lib/epochtalk_server/models/category.ex +++ b/lib/epochtalk_server/models/category.ex @@ -18,10 +18,22 @@ defmodule EpochtalkServer.Models.Category do many_to_many :boards, Board, join_through: BoardMapping end - def changeset(board, attrs) do - board + def changeset(category, attrs) do + category |> cast(attrs, [:id, :name, :view_order, :viewable_by, :postable_by, :created_at, :imported_at, :updated_at, :meta]) |> unique_constraint(:id, name: :categories_pkey) end - def insert(%Category{} = category), do: Repo.insert(category) + def insert(%Category{} = cat), do: Repo.insert(cat) + + def changeset_for_board_mapping(category, attrs) do + category + |> cast(attrs, [:id, :name, :view_order, :viewable_by, :postable_by]) + |> unique_constraint(:id, name: :categories_pkey) + end + + def update_for_board_mapping(%{ id: id } = category_map) do + %Category{ id: id } + |> changeset_for_board_mapping(category_map) + |> Repo.update + end end From 35006a49b80c1e7d7389bc4350868bd90a35af37 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 12 Sep 2022 15:00:36 -1000 Subject: [PATCH 050/231] feat(board-mapping-model): port and implement update, for use with seed --- lib/epochtalk_server/models/board_mapping.ex | 46 ++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 lib/epochtalk_server/models/board_mapping.ex diff --git a/lib/epochtalk_server/models/board_mapping.ex b/lib/epochtalk_server/models/board_mapping.ex new file mode 100644 index 00000000..818030ad --- /dev/null +++ b/lib/epochtalk_server/models/board_mapping.ex @@ -0,0 +1,46 @@ +defmodule EpochtalkServer.Models.BoardMapping do + use Ecto.Schema + import Ecto.Changeset + import Ecto.Query, only: [from: 2] + alias EpochtalkServer.Repo + alias EpochtalkServer.Models.Board + alias EpochtalkServer.Models.BoardMapping + alias EpochtalkServer.Models.Category + + @primary_key false + schema "board_mapping" do + belongs_to :board, Board, primary_key: true + belongs_to :parent, Board, primary_key: true + belongs_to :category, Category, primary_key: true + field :view_order, :integer + end + + def changeset(board_mapping, attrs) do + board_mapping + |> cast(attrs, [:board_id, :parent_id, :category_id, :view_order]) + |> foreign_key_constraint(:parent_id, name: :board_mapping_parent_id_fkey) + |> unique_constraint([:board_id, :parent_id], name: :board_mapping_board_id_parent_id_index) + |> unique_constraint([:board_id, :category_id], name: :board_mapping_board_id_category_id_index) + end + def insert(%BoardMapping{} = board_mapping), do: Repo.insert(board_mapping) + + def delete_board(id) do + query = from bm in BoardMapping, + where: bm.board_id == ^id + Repo.delete_all(query) + end + + def update(board_mapping_list) do + Repo.transaction(fn -> + Enum.each(board_mapping_list, &BoardMapping.update(&1, Map.get(&1, :type))) + end) + end + def update(cat, "category"), do: Category.update_for_board_mapping(cat) + def update(uncat, "uncategorized"), do: delete_board(Map.get(uncat, :id)) + def update(board, "board") do + delete_board(Map.get(board, :id)) + %BoardMapping{} + |> changeset(Map.put(board, :board_id, Map.get(board, :id))) + |> Repo.insert + end +end From 7e4ede59c3fd07791adb5d08b89cf378061ac3ee Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 12 Sep 2022 16:05:47 -1000 Subject: [PATCH 051/231] feat(metadata-boards): add metadata boards model --- .../models/metadata_boards.ex | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 lib/epochtalk_server/models/metadata_boards.ex diff --git a/lib/epochtalk_server/models/metadata_boards.ex b/lib/epochtalk_server/models/metadata_boards.ex new file mode 100644 index 00000000..7afa4f4d --- /dev/null +++ b/lib/epochtalk_server/models/metadata_boards.ex @@ -0,0 +1,28 @@ +defmodule EpochtalkServer.Models.MetadataBoards do + use Ecto.Schema + import Ecto.Changeset + alias EpochtalkServer.Models.Board + + @schema_prefix "metadata" + schema "boards" do + belongs_to :board, Board + field :post_count, :integer + field :thread_count, :integer + field :total_post, :integer + field :total_thread_count, :integer + field :last_post_username, :string + field :last_post_created_at, :naive_datetime + field :last_thread_id, :integer + field :last_thread_title, :string + field :last_post_position, :integer + end + + def changeset(permission, attrs \\ %{}) do + permission + |> cast(attrs, [:id, :board_id, :post_count, :thread_count, + :total_post, :total_thread_count, :last_post_username, + :last_post_created_at, :last_thread_id, :last_thread_title, + :last_post_position]) + |> validate_required([:board_id]) + end +end From b1794d7e4e82ded1b60d00c94ddb33869a7fccaa Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 13 Sep 2022 09:48:37 -1000 Subject: [PATCH 052/231] feat(boards): implement create board --- lib/epochtalk_server/models/board.ex | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/epochtalk_server/models/board.ex b/lib/epochtalk_server/models/board.ex index 79a79a49..c12ed7f8 100644 --- a/lib/epochtalk_server/models/board.ex +++ b/lib/epochtalk_server/models/board.ex @@ -5,6 +5,7 @@ defmodule EpochtalkServer.Models.Board do alias EpochtalkServer.Models.Category alias EpochtalkServer.Models.BoardMapping alias EpochtalkServer.Models.Board + alias EpochtalkServer.Models.MetadataBoards schema "boards" do field :name, :string @@ -14,6 +15,7 @@ defmodule EpochtalkServer.Models.Board do field :thread_count, :integer field :viewable_by, :integer field :postable_by, :integer + field :right_to_left, :boolean field :created_at, :naive_datetime field :imported_at, :naive_datetime field :updated_at, :naive_datetime @@ -28,5 +30,25 @@ defmodule EpochtalkServer.Models.Board do |> unique_constraint(:id, name: :boards_pkey) |> unique_constraint(:slug, name: :boards_slug_index) end + def create_changeset(board, attrs) do + now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) + attrs = attrs + |> Map.put(:created_at, now) + |> Map.put(:updated_at, now) + board + |> cast(attrs, [:name, :slug, :description, :viewable_by, :postable_by, :right_to_left, :created_at, :updated_at, :meta]) + |> unique_constraint(:id, name: :boards_pkey) + |> unique_constraint(:slug, name: :boards_slug_index) + end def insert(%Board{} = board), do: Repo.insert(board) + def create(board) do + board_cs = create_changeset(%Board{}, board) + case Repo.insert(board_cs) do + {:ok, db_board} -> case MetadataBoards.insert(%MetadataBoards{ board_id: db_board.id}) do + {:ok, _} -> db_board + {:error, cs} -> {:error, cs} + end + {:error, cs} -> {:error, cs} + end + end end From 73661c1f749860dab0eaafee57aac5e8a26a2562 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 13 Sep 2022 09:49:56 -1000 Subject: [PATCH 053/231] feat(category): implement create category --- lib/epochtalk_server/models/category.ex | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server/models/category.ex b/lib/epochtalk_server/models/category.ex index ac2945a9..bb28a23a 100644 --- a/lib/epochtalk_server/models/category.ex +++ b/lib/epochtalk_server/models/category.ex @@ -23,11 +23,29 @@ defmodule EpochtalkServer.Models.Category do |> cast(attrs, [:id, :name, :view_order, :viewable_by, :postable_by, :created_at, :imported_at, :updated_at, :meta]) |> unique_constraint(:id, name: :categories_pkey) end - def insert(%Category{} = cat), do: Repo.insert(cat) + def insert(%Category{} = category), do: Repo.insert(category) + + def create_changeset(category, attrs) do + now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) + attrs = attrs + |> Map.put(:created_at, now) + |> Map.put(:updated_at, now) + category + |> cast(attrs, [:name, :viewable_by, :created_at, :updated_at]) + end + def create(category) do + category_cs = create_changeset(%Category{}, category) + db_cat = Repo.insert!(category_cs) + %{ + id: db_cat.id, + name: db_cat.name, + viewable_by: db_cat.viewable_by + } + end def changeset_for_board_mapping(category, attrs) do category - |> cast(attrs, [:id, :name, :view_order, :viewable_by, :postable_by]) + |> cast(attrs, [:id, :name, :view_order, :viewable_by]) |> unique_constraint(:id, name: :categories_pkey) end From da8b65d6910eed1deae56d22acf820ea8f136091 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 13 Sep 2022 09:51:04 -1000 Subject: [PATCH 054/231] feat(metadata-boards): implement insert for metadataboards --- lib/epochtalk_server/models/metadata_boards.ex | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/epochtalk_server/models/metadata_boards.ex b/lib/epochtalk_server/models/metadata_boards.ex index 7afa4f4d..c11e116a 100644 --- a/lib/epochtalk_server/models/metadata_boards.ex +++ b/lib/epochtalk_server/models/metadata_boards.ex @@ -1,7 +1,9 @@ defmodule EpochtalkServer.Models.MetadataBoards do use Ecto.Schema import Ecto.Changeset + alias EpochtalkServer.Repo alias EpochtalkServer.Models.Board + alias EpochtalkServer.Models.MetadataBoards @schema_prefix "metadata" schema "boards" do @@ -25,4 +27,5 @@ defmodule EpochtalkServer.Models.MetadataBoards do :last_post_position]) |> validate_required([:board_id]) end + def insert(%MetadataBoards{} = metadata_boards), do: Repo.insert(metadata_boards) end From a7b5778abd292746848dba97f715eed9342c3048 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 13 Sep 2022 09:52:27 -1000 Subject: [PATCH 055/231] feat(roles): implement get_banned_role_id and get_newbie_role_id, refactor move call to get_default internally into roles --- lib/epochtalk_server/models/role.ex | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server/models/role.ex b/lib/epochtalk_server/models/role.ex index 6032fc2d..47e08362 100644 --- a/lib/epochtalk_server/models/role.ex +++ b/lib/epochtalk_server/models/role.ex @@ -38,13 +38,18 @@ defmodule EpochtalkServer.Models.Role do |> Repo.update end - def get_default(), do: by_lookup("user") + def get_banned_role_id(), do: Repo.one(from(r in Role, select: r.id, where: r.lookup == "banned")) + def get_newbie_role_id(), do: Repo.one(from(r in Role, select: r.id, where: r.lookup == "newbie")) def by_user_id(user_id) do query = from ru in RoleUser, join: r in Role, where: ru.user_id == ^user_id and r.id == ru.role_id, select: r, order_by: [asc: r.priority] - Repo.all(query) + case Repo.all(query) do + [] -> [get_default()] + result -> result + end end + defp get_default(), do: by_lookup("user") end From bf93807ac0d45ec28796bd6d12c4265da595e48d Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 13 Sep 2022 09:53:12 -1000 Subject: [PATCH 056/231] feat(roles-users): implement delete for roles users --- lib/epochtalk_server/models/role_user.ex | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/epochtalk_server/models/role_user.ex b/lib/epochtalk_server/models/role_user.ex index 7729fd7f..aa57327b 100644 --- a/lib/epochtalk_server/models/role_user.ex +++ b/lib/epochtalk_server/models/role_user.ex @@ -1,8 +1,11 @@ defmodule EpochtalkServer.Models.RoleUser do use Ecto.Schema import Ecto.Changeset + import Ecto.Query, only: [from: 2] + alias EpochtalkServer.Repo alias EpochtalkServer.Models.User alias EpochtalkServer.Models.Role + alias EpochtalkServer.Models.RoleUser schema "roles_users" do belongs_to :user, User @@ -14,4 +17,10 @@ defmodule EpochtalkServer.Models.RoleUser do |> cast(attrs, [:user_id, :role_id]) |> validate_required([:user_id, :role_id]) end + + def delete(role_id, user_id) do + query = from ru in RoleUser, + where: ru.role_id == ^role_id and ru.user_id == ^user_id + Repo.delete_all(query) + end end From 9d043bb164cbfc5ef67e5103280baa9917480f5b Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 13 Sep 2022 09:54:42 -1000 Subject: [PATCH 057/231] feat(user): implement clear_malicious_score and refactor code to fetch users roles --- lib/epochtalk_server/models/user.ex | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/epochtalk_server/models/user.ex b/lib/epochtalk_server/models/user.ex index a5db87cd..b7b219f5 100644 --- a/lib/epochtalk_server/models/user.ex +++ b/lib/epochtalk_server/models/user.ex @@ -39,6 +39,10 @@ defmodule EpochtalkServer.Models.User do def with_username_exists?(username), do: Repo.exists?(from u in User, where: u.username == ^username) def with_email_exists?(email), do: Repo.exists?(from u in User, where: u.email == ^email) def by_id(id) when is_integer(id), do: Repo.get_by(User, id: id) + def clear_malicious_score(id) when is_integer(id) do + from(u in User, where: u.id == ^id) + |> Repo.update_all(set: [malicious_score: nil]) + end def by_username(username) when is_binary(username) do query = from u in User, left_join: p in Profile, @@ -85,11 +89,8 @@ defmodule EpochtalkServer.Models.User do where: u.username == ^username if user = Repo.one(query) do - # set all user's roles, if the have none set default role - user = case length(all_users_roles = Role.by_user_id(user.id)) > 0 do - true -> Map.put(user, :roles, all_users_roles) - false -> Map.put(user, :roles, [Role.get_default()]) - end + # set all user's roles + user = Map.put(user, :roles, Role.by_user_id(user.id)) # set primary role info primary_role = List.first(user[:roles]) hc = primary_role.highlight_color From 1d285c6a19ca4cd43748b769e23e304fccc8afce Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 13 Sep 2022 09:56:27 -1000 Subject: [PATCH 058/231] feat(invitations): implement invitations and implement create method --- lib/epochtalk_server/models/invitation.ex | 32 +++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 lib/epochtalk_server/models/invitation.ex diff --git a/lib/epochtalk_server/models/invitation.ex b/lib/epochtalk_server/models/invitation.ex new file mode 100644 index 00000000..71cd2e98 --- /dev/null +++ b/lib/epochtalk_server/models/invitation.ex @@ -0,0 +1,32 @@ +defmodule EpochtalkServer.Models.Invitation do + use Ecto.Schema + import Ecto.Changeset + import Ecto.Query, only: [from: 2] + alias EpochtalkServer.Repo + alias EpochtalkServer.Models.Invitation + + @primary_key false + schema "invitations" do + field :email, :string + field :hash, :string + field :created_at, :naive_datetime + end + + def create_changeset(invitation, attrs \\ %{}) do + now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) + attrs = attrs + |> Map.put(:created_at, now) + |> Map.put(:hash, Base.url_encode64(:crypto.strong_rand_bytes(20), padding: false)) + invitation + |> cast(attrs, [:email, :hash, :created_at]) + |> validate_required([:email, :hash, :created_at]) + |> unique_constraint(:email, name: :invitations_email_index) + |> check_constraint(:email, + name: :invitations_email_check, + message: "must be 255 characters or less" + ) + end + + def create(email), do: Repo.insert(create_changeset(%Invitation{}, %{email: email})) + def delete(email), do: Repo.delete_all(from(i in Invitation, where: i.email == ^email)) +end From 2e6e74f92e5d502a022155568b0b5d742afe1211 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 13 Sep 2022 09:57:32 -1000 Subject: [PATCH 059/231] feat(ban): implement ban model and unban function for login --- lib/epochtalk_server/models/ban.ex | 46 ++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 lib/epochtalk_server/models/ban.ex diff --git a/lib/epochtalk_server/models/ban.ex b/lib/epochtalk_server/models/ban.ex new file mode 100644 index 00000000..6729144a --- /dev/null +++ b/lib/epochtalk_server/models/ban.ex @@ -0,0 +1,46 @@ +defmodule EpochtalkServer.Models.Ban do + use Ecto.Schema + import Ecto.Changeset + alias EpochtalkServer.Repo + alias EpochtalkServer.Models.User + alias EpochtalkServer.Models.Role + alias EpochtalkServer.Models.RoleUser + alias EpochtalkServer.Models.Ban + + @schema_prefix "users" + schema "bans" do + belongs_to :user, User, primary_key: true + field :expiration, :naive_datetime + field :created_at, :naive_datetime + field :updated_at, :naive_datetime + end + + def changeset(ban, attrs \\ %{}) do + ban + |> cast(attrs, [:id, :user_id, :expiration, :created_at, :updated_at]) + |> validate_required([:user_id]) + end + + def unban_changeset(ban, attrs \\ %{}) do + now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) + attrs = attrs + |> Map.put(:expiration, now) + |> Map.put(:updated_at, now) + ban + |> cast(attrs, [:user_id, :expiration, :updated_at]) + |> validate_required([:user_id]) + end + + def insert(%Ban{} = ban), do: Repo.insert(ban) + def unban(user_id) when is_integer(user_id) do + Repo.transaction(fn -> + db_ban = case Repo.get_by(Ban, user_id: user_id) do + nil -> %{ user_id: user_id } + cs -> Repo.update!(unban_changeset(cs, %{ user_id: user_id })) + end # unban the user + User.clear_malicious_score(user_id) # clear user malicious score + RoleUser.delete(Role.get_banned_role_id, user_id) # delete ban role from user + Map.put(db_ban, :roles, Role.by_user_id(user_id)) # append user roles + end) + end +end From 2db6b3f31a0f77c3e81562eed9473c80a557d209 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 13 Sep 2022 15:35:37 -1000 Subject: [PATCH 060/231] feat(banned-addresses): port banned addresses and upsert function, for future use for caclulating users malicious score, used during auth --- lib/epochtalk_server/models/banned_address.ex | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 lib/epochtalk_server/models/banned_address.ex diff --git a/lib/epochtalk_server/models/banned_address.ex b/lib/epochtalk_server/models/banned_address.ex new file mode 100644 index 00000000..c3e04355 --- /dev/null +++ b/lib/epochtalk_server/models/banned_address.ex @@ -0,0 +1,133 @@ +defmodule EpochtalkServer.Models.BannedAddress do + use Ecto.Schema + import Ecto.Changeset + import Ecto.Query, only: [from: 2] + alias EpochtalkServer.Repo + alias EpochtalkServer.Models.BannedAddress + + @primary_key false + schema "banned_addresses" do + field :hostname, :string + field :ip1, :integer + field :ip2, :integer + field :ip3, :integer + field :ip4, :integer + field :weight, :decimal + field :decay, :boolean, default: false + field :imported_at, :naive_datetime + field :created_at, :naive_datetime + field :updates, {:array, :naive_datetime} + end + + def changeset(banned_address, attrs \\ %{}) do + now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) + attrs = attrs + |> Map.put(:created_at, now) + |> Map.put(:decay, (if decay = Map.get(attrs, :decay), do: decay, else: false)) # default false + |> Map.put(:updates, (if updates = Map.get(banned_address, :updates), do: updates ++ [now], else: [])) + case Map.get(attrs, :hostname) do + nil -> ip = String.split(attrs.ip, ".") + attrs = attrs + |> Map.put(:ip1, Enum.at(ip, 0)) + |> Map.put(:ip2, Enum.at(ip, 1)) + |> Map.put(:ip3, Enum.at(ip, 2)) + |> Map.put(:ip4, Enum.at(ip, 3)) + changeset_ip(banned_address, attrs) + _ -> changeset_hostname(banned_address, attrs) + end + end + def changeset_hostname(banned_address, attrs \\ %{}) do + banned_address + |> cast(attrs, [:hostname, :weight, :decay, :imported_at, :created_at, :updates]) + |> validate_required([:hostname, :weight, :decay, :created_at, :updates]) + |> unique_constraint(:hostname, name: :banned_addresses_hostname_index) + end + def changeset_ip(banned_address, attrs \\ %{}) do + cs_data = banned_address + |> cast(attrs, [:ip1, :ip2, :ip3, :ip4, :weight, :decay, :imported_at, :created_at, :updates]) + |> validate_required([:ip1, :ip2, :ip3, :ip4, :weight, :decay, :created_at, :updates]) + |> unique_constraint([:ip1, :ip2, :ip3, :ip4], name: :banned_addresses_unique_ip_constraint) + # have changeset calculate new decayed weight if necessary + updated_cs = Map.merge(cs_data.data, cs_data.changes) + case Map.get(banned_address, :decay) and updated_cs.decay do + true -> weight = calculate_score_decay(banned_address) # get existing decayed weight since ip was last seen + # since this ip has been previously banned run through algorithm + # min(2 * old_score, old_score + 1000) to get new weight where + # old_score accounts for previous decay + weight = min(2 * weight, weight + 1000) + change(cs_data, %{ weight: weight }) + false -> cs_data + end + end + + def upsert(address_list) when is_list(address_list) do + Repo.transaction(fn -> + Enum.each(address_list ,&BannedAddress.upsert(&1)) + end) + end + def upsert(banned_address) do + case Map.get(banned_address, :hostname) do + # IP type banned address + nil -> + ip = String.split(banned_address.ip, ".") + ip1 = Enum.at(ip, 0) + ip2 = Enum.at(ip, 1) + ip3 = Enum.at(ip, 2) + ip4 = Enum.at(ip, 3) + db_banned_address = Repo.get_by(BannedAddress, %{ip1: ip1, ip2: ip2, ip3: ip3, ip4: ip4}) + if db_banned_address do # update + cs_data = changeset(db_banned_address, banned_address) + updated_cs = Map.merge(cs_data.data, cs_data.changes) + from(ba in BannedAddress, where: ba.ip1 == ^ip1 and ba.ip2 == ^ip2 and ba.ip3 == ^ip3 and ba.ip4 == ^ip4) + |> Repo.update_all(set: [ + weight: Map.get(updated_cs, :weight), + decay: Map.get(updated_cs, :decay), + updates: Map.get(updated_cs, :updates) + ]) + {:ok, updated_cs} + else Repo.insert(changeset(%BannedAddress{}, banned_address), returning: true) end #insert + # hostname type banned address + hostname -> + if db_banned_address = Repo.get_by(BannedAddress, hostname: hostname) do # update + # grab changes from changeset, this is a workaround since + # we can't use Repo.update, because there is no primary key + cs_data = changeset(db_banned_address, banned_address) + updated_cs = Map.merge(cs_data.data, cs_data.changes) + from(ba in BannedAddress, where: ba.hostname == ^hostname) + |> Repo.update_all(set: [ + weight: Map.get(updated_cs, :weight), + decay: Map.get(updated_cs, :decay), + updates: Map.get(updated_cs, :updates) + ]) + {:ok, updated_cs} + else Repo.insert(changeset(%BannedAddress{}, banned_address), returning: true) end # insert + end + end + + # Calculates decay given MS and a weight + # Decay algorithm is 0.8897*curWeight^0.9644 run weekly + # This will calculate decay given the amount of time that has passed + # in ms since the weight was last updated + defp decay_for_time(time, weight) do + weight = Decimal.to_float(weight) + one_week = 1000 * 60 * 60 * 24 * 7 + weeks = time / one_week + a = 0.8897 + r = 0.9644 + (a ** (((r ** weeks) - 1) / (r - 1))) * (weight ** (r ** weeks)) + end + + # returns the decayed score given a banned address + # no banned address data found return 0 + def calculate_score_decay(), do: 0 + def calculate_score_decay(banned_address) when banned_address == %{}, do: 0 + def calculate_score_decay(banned_address) when is_nil(banned_address), do: 0 + # banned address data found and decays, calculate + def calculate_score_decay(%{ decay: true, updates: updates, weight: weight, created_at: created_at } = _banned_address) do + last_update_date = if length(updates) > 0, do: List.last(updates), else: created_at + diff_ms = abs(NaiveDateTime.diff(last_update_date, NaiveDateTime.utc_now()) * 1000) + decay_for_time(diff_ms, weight) + end + # banned address data found, but does not decay + def calculate_score_decay(banned_address), do: banned_address.weight +end From 5e403963fcb51df06d193260cf2ba9f5ae6286bd Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 15 Sep 2022 14:38:46 -1000 Subject: [PATCH 061/231] feat(malicious-score): implement code in BannedAddress model to calculate malicious score, required for registration route --- lib/epochtalk_server/models/banned_address.ex | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/lib/epochtalk_server/models/banned_address.ex b/lib/epochtalk_server/models/banned_address.ex index c3e04355..83f9ef82 100644 --- a/lib/epochtalk_server/models/banned_address.ex +++ b/lib/epochtalk_server/models/banned_address.ex @@ -104,6 +104,52 @@ defmodule EpochtalkServer.Models.BannedAddress do end end + def calculate_malicious_score_from_ip(ip) do + case :inet.parse_address(to_charlist(ip)) do + {:ok, ip} -> + hostname_score = case :inet_res.gethostbyaddr(ip) do + {:ok, host} -> hostname_from_host(host) |> calculate_hostname_score + {:error, _} -> 0 # no hostname found, return 0 for hostname score + end + ip32_score = calculate_ip32_score(ip) + ip24_score = calculate_ip24_score(ip) + ip16_score = calculate_ip16_score(ip) + # calculate malicious score using all scores + hostname_score + ip32_score + 0.04 + ip24_score + 0.0016 + ip16_score + {:error, _} -> 0 # invalid ip address, return 0 for malicious score + end + end + + defp hostname_from_host({ _, name, _, _, _, _ }), do: List.to_string(name) + defp calculate_hostname_score(hostname) do + (from ba in BannedAddress, + where: like(ba.hostname, ^hostname), + select: %{weight: ba.weight, decay: ba.decay, created_at: ba.created_at, updates: ba.updates}) + |> Repo.one + |> calculate_score_decay + end + defp calculate_ip32_score({ ip1, ip2, ip3, ip4 }) do + (from ba in BannedAddress, + where: ba.ip1 == ^ip1 and ba.ip2 == ^ip2 and ba.ip3 == ^ip3 and ba.ip4 == ^ip4, + select: %{weight: ba.weight, decay: ba.decay, created_at: ba.created_at, updates: ba.updates}) + |> Repo.one + |> calculate_score_decay + end + defp calculate_ip24_score({ ip1, ip2, ip3, _ }) do + (from ba in BannedAddress, + where: ba.ip1 == ^ip1 and ba.ip2 == ^ip2 and ba.ip3 == ^ip3, + select: %{weight: ba.weight, decay: ba.decay, created_at: ba.created_at, updates: ba.updates}) + |> Repo.one + |> calculate_score_decay + end + defp calculate_ip16_score({ ip1, ip2, _, _ }) do + (from ba in BannedAddress, + where: ba.ip1 == ^ip1 and ba.ip2 == ^ip2, + select: %{weight: ba.weight, decay: ba.decay, created_at: ba.created_at, updates: ba.updates}) + |> Repo.one + |> calculate_score_decay + end + # Calculates decay given MS and a weight # Decay algorithm is 0.8897*curWeight^0.9644 run weekly # This will calculate decay given the amount of time that has passed @@ -129,5 +175,5 @@ defmodule EpochtalkServer.Models.BannedAddress do decay_for_time(diff_ms, weight) end # banned address data found, but does not decay - def calculate_score_decay(banned_address), do: banned_address.weight + def calculate_score_decay(banned_address), do: Decimal.to_float(banned_address.weight) end From 780c2d788b147be5ba48cc91fe7fe85ec3c60282 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 16 Sep 2022 08:55:52 -1000 Subject: [PATCH 062/231] refactor(role-permission): use shorthand key: value converted from :key => value --- lib/epochtalk_server/models/role_permission.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server/models/role_permission.ex b/lib/epochtalk_server/models/role_permission.ex index 3d442d86..54d7d011 100644 --- a/lib/epochtalk_server/models/role_permission.ex +++ b/lib/epochtalk_server/models/role_permission.ex @@ -63,9 +63,9 @@ defmodule EpochtalkServer.Models.RolePermission do where: rp.role_id == ^role_id) |> Repo.all # filter for true permissions - |> Enum.filter(fn %{:value => value, :modified => modified} -> (value || modified) && !(value && modified) end) + |> Enum.filter(fn %{value: value, modified: modified} -> (value || modified) && !(value && modified) end) # convert results to map; keyed by permissions_path - |> Enum.reduce(%{}, fn %{:permission_path => permission_path, :value => value}, acc -> Map.put(acc, permission_path, value) end) + |> Enum.reduce(%{}, fn %{permission_path: permission_path, value: value}, acc -> Map.put(acc, permission_path, value) end) |> Iteraptor.from_flatmap end # for server-side role-loading use, only runs if roles permissions table is currently empty From 7873f7da9ac1ad0dce6c49732726303d501c7b83 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 16 Sep 2022 09:00:25 -1000 Subject: [PATCH 063/231] feat(seed-user): add command line documentation for usage --- priv/repo/seed_user.exs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/priv/repo/seed_user.exs b/priv/repo/seed_user.exs index c4d3e002..b14ba33c 100644 --- a/priv/repo/seed_user.exs +++ b/priv/repo/seed_user.exs @@ -6,6 +6,8 @@ System.argv() User.create_user(%{username: username, email: email, password: password}, admin) [username, email, password] -> User.create_user(%{username: username, email: email, password: password}) + _ -> + IO.puts("Usage: mix seed.user [admin]") end |> case do {:ok, _} -> IO.puts("Successfully seeded user") From 561bfbf4884c20a922d1d4ceeb2b5179771deb5f Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 16 Sep 2022 09:39:25 -1000 Subject: [PATCH 064/231] style(category): fix whitespace --- lib/epochtalk_server/models/category.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epochtalk_server/models/category.ex b/lib/epochtalk_server/models/category.ex index bb28a23a..cb6c1be6 100644 --- a/lib/epochtalk_server/models/category.ex +++ b/lib/epochtalk_server/models/category.ex @@ -5,7 +5,7 @@ defmodule EpochtalkServer.Models.Category do alias EpochtalkServer.Models.Board alias EpochtalkServer.Models.Category alias EpochtalkServer.Models.BoardMapping - + schema "categories" do field :name, :string field :view_order, :integer From d1788e3372d6ae1a7b2674bb985785797183840b Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 16 Sep 2022 09:40:51 -1000 Subject: [PATCH 065/231] feat(seed): start forum seed --- priv/repo/seed_forum.exs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 priv/repo/seed_forum.exs diff --git a/priv/repo/seed_forum.exs b/priv/repo/seed_forum.exs new file mode 100644 index 00000000..370b224d --- /dev/null +++ b/priv/repo/seed_forum.exs @@ -0,0 +1,8 @@ +alias EpochtalkServer.Models.Category + + +Category.insert(%Category{name: "General"}) +|> IO.inspect +# |> case do +# +# end From 729a7e1313a922b30f486ed648d3b7d6dd75d703 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 16 Sep 2022 09:59:57 -1000 Subject: [PATCH 066/231] feat(mix): add seed forum --- mix.exs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 059f2238..c02c24d0 100644 --- a/mix.exs +++ b/mix.exs @@ -65,7 +65,8 @@ defmodule EpochtalkServer.MixProject do "ecto.reset": ["ecto.drop", "ecto.setup"], "db.migrate": ["ecto.migrate", "ecto.dump"], "db.rollback": ["ecto.rollback", "ecto.dump"], - "seed.all": ["seed.prp", "seed.permissions", "seed.roles", "seed.rp"], + "seed.all": ["seed.prp", "seed.permissions", "seed.roles", "seed.rp", "seed.forum"], + "seed.forum": ["run priv/repo/seed_forum.exs"], "seed.permissions": ["run priv/repo/seed_permissions.exs"], "seed.prp": ["run priv/repo/process_roles_permissions.exs"], "seed.roles": ["run priv/repo/seed_roles.exs"], From 88650ccf1595b147b952078e0b4bd210c96833b2 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 16 Sep 2022 10:00:38 -1000 Subject: [PATCH 067/231] refactor(seed-forum): extract category name, handle return --- priv/repo/seed_forum.exs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/priv/repo/seed_forum.exs b/priv/repo/seed_forum.exs index 370b224d..06f4396c 100644 --- a/priv/repo/seed_forum.exs +++ b/priv/repo/seed_forum.exs @@ -1,8 +1,9 @@ alias EpochtalkServer.Models.Category +category_name = "General" - -Category.insert(%Category{name: "General"}) -|> IO.inspect -# |> case do -# -# end +Category.insert(%Category{name: category_name}) +|> case do + {:ok, category} -> + IO.inspect(category) + _ -> IO.puts("Seed failed, unable to create Category with name #{category_name}") +end From 5c68385d9954e81466e48b27179426ed620e70fb Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 16 Sep 2022 10:14:37 -1000 Subject: [PATCH 068/231] feat(seed-forum): add board seed, refactor --- priv/repo/seed_forum.exs | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/priv/repo/seed_forum.exs b/priv/repo/seed_forum.exs index 06f4396c..1b1725f7 100644 --- a/priv/repo/seed_forum.exs +++ b/priv/repo/seed_forum.exs @@ -1,9 +1,28 @@ alias EpochtalkServer.Models.Category category_name = "General" +category = %Category{ + name: category_name +} -Category.insert(%Category{name: category_name}) +board_name = "General Discussion" +board_description = "Every forum's got one; talk about anything here" +board = %Board{ + name: board_name, + description: board_description +} + +seeded_category = Category.insert(category_seed) +|> case do + {:ok, c} -> c + _ -> + IO.puts("Seed failed, unable to create Category with name #{category_name}") + Process.exit(self, :normal) +end + +seeded_board = Board.insert(board_seed) |> case do - {:ok, category} -> - IO.inspect(category) - _ -> IO.puts("Seed failed, unable to create Category with name #{category_name}") + {:ok, b} -> b + _ -> + IO.puts("Seed failed, unable to create Board #{board_name}, #{board_description}") + Process.exit(self, :normal) end From 65e6cde546507572832a5eed848fbcf00bc46e8d Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 16 Sep 2022 10:15:13 -1000 Subject: [PATCH 069/231] fix(seed-forum): alias board model --- priv/repo/seed_forum.exs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/priv/repo/seed_forum.exs b/priv/repo/seed_forum.exs index 1b1725f7..d7a2a30e 100644 --- a/priv/repo/seed_forum.exs +++ b/priv/repo/seed_forum.exs @@ -1,4 +1,6 @@ alias EpochtalkServer.Models.Category +alias EpochtalkServer.Models.Board + category_name = "General" category = %Category{ name: category_name From fcb52117139288d70c1133e8629b5affd0f207bb Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 16 Sep 2022 10:15:56 -1000 Subject: [PATCH 070/231] fix(seed-forum): variable names --- priv/repo/seed_forum.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/priv/repo/seed_forum.exs b/priv/repo/seed_forum.exs index d7a2a30e..a67b5934 100644 --- a/priv/repo/seed_forum.exs +++ b/priv/repo/seed_forum.exs @@ -13,7 +13,7 @@ board = %Board{ description: board_description } -seeded_category = Category.insert(category_seed) +seeded_category = Category.insert(category) |> case do {:ok, c} -> c _ -> @@ -21,7 +21,7 @@ seeded_category = Category.insert(category_seed) Process.exit(self, :normal) end -seeded_board = Board.insert(board_seed) +seeded_board = Board.insert(board) |> case do {:ok, b} -> b _ -> From edca5ee9b6330ce9645e59015e42ab337c8f7555 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 16 Sep 2022 10:20:21 -1000 Subject: [PATCH 071/231] fix(seed-forum): add slug to board seed --- priv/repo/seed_forum.exs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/priv/repo/seed_forum.exs b/priv/repo/seed_forum.exs index a67b5934..434f0b45 100644 --- a/priv/repo/seed_forum.exs +++ b/priv/repo/seed_forum.exs @@ -8,9 +8,11 @@ category = %Category{ board_name = "General Discussion" board_description = "Every forum's got one; talk about anything here" +board_slug = "general-discussion" board = %Board{ name: board_name, - description: board_description + description: board_description, + slug: board_slug } seeded_category = Category.insert(category) From 153be8a1b3bbd9b2f619539df00a8aad49e6acad Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 16 Sep 2022 11:48:04 -1000 Subject: [PATCH 072/231] feat(seed-forum): seed board mapping --- priv/repo/seed_forum.exs | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/priv/repo/seed_forum.exs b/priv/repo/seed_forum.exs index 434f0b45..837fddbe 100644 --- a/priv/repo/seed_forum.exs +++ b/priv/repo/seed_forum.exs @@ -1,32 +1,46 @@ alias EpochtalkServer.Models.Category alias EpochtalkServer.Models.Board +alias EpochtalkServer.Models.BoardMapping category_name = "General" -category = %Category{ +category = %{ name: category_name } board_name = "General Discussion" board_description = "Every forum's got one; talk about anything here" board_slug = "general-discussion" -board = %Board{ +board = %{ name: board_name, description: board_description, slug: board_slug } -seeded_category = Category.insert(category) -|> case do - {:ok, c} -> c - _ -> - IO.puts("Seed failed, unable to create Category with name #{category_name}") - Process.exit(self, :normal) -end +category_id = Category.create(category).id -seeded_board = Board.insert(board) +board_id = Board.create(board) |> case do - {:ok, b} -> b - _ -> - IO.puts("Seed failed, unable to create Board #{board_name}, #{board_description}") + {:error, m} -> + IO.puts("Seed failed") + IO.inspect(m) Process.exit(self, :normal) + b -> b.id end + +board_mapping = [ + %{ + id: category_id, + name: category_name, + type: "category", + view_order: 0 + }, + %{ + id: board_id, + name: board_name, + type: "board", + category_id: category_id, + view_order: 1 + } +] + +BoardMapping.update(board_mapping) From 4eb041b3c65b4d926935443a48cac0374472e616 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Fri, 16 Sep 2022 11:59:37 -1000 Subject: [PATCH 073/231] refactor(seed-forum): use transaction --- priv/repo/seed_forum.exs | 55 ++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/priv/repo/seed_forum.exs b/priv/repo/seed_forum.exs index 837fddbe..588a9007 100644 --- a/priv/repo/seed_forum.exs +++ b/priv/repo/seed_forum.exs @@ -1,6 +1,7 @@ alias EpochtalkServer.Models.Category alias EpochtalkServer.Models.Board alias EpochtalkServer.Models.BoardMapping +alias EpochtalkServer.Repo category_name = "General" category = %{ @@ -16,31 +17,35 @@ board = %{ slug: board_slug } -category_id = Category.create(category).id +Repo.transaction(fn -> + category_id = Category.create(category).id -board_id = Board.create(board) -|> case do - {:error, m} -> - IO.puts("Seed failed") - IO.inspect(m) - Process.exit(self, :normal) - b -> b.id -end + board_id = Board.create(board) + |> case do + b -> b.id + end -board_mapping = [ - %{ - id: category_id, - name: category_name, - type: "category", - view_order: 0 - }, - %{ - id: board_id, - name: board_name, - type: "board", - category_id: category_id, - view_order: 1 - } -] + board_mapping = [ + %{ + id: category_id, + name: category_name, + type: "category", + view_order: 0 + }, + %{ + id: board_id, + name: board_name, + type: "board", + category_id: category_id, + view_order: 1 + } + ] -BoardMapping.update(board_mapping) + BoardMapping.update(board_mapping) +end) +|> case do + {:ok, _} -> IO.puts("Success") + {:error, error} -> + IO.puts("Error during seed") + IO.inspect(error) +end From 65ceb2e8989ae58101a879750c46a6d742ce202a Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Fri, 16 Sep 2022 12:34:12 -1000 Subject: [PATCH 074/231] refactor(role-user): rename delete->delete_user_role --- lib/epochtalk_server/models/role_user.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epochtalk_server/models/role_user.ex b/lib/epochtalk_server/models/role_user.ex index 93c5c5d2..9178542a 100644 --- a/lib/epochtalk_server/models/role_user.ex +++ b/lib/epochtalk_server/models/role_user.ex @@ -26,7 +26,7 @@ defmodule EpochtalkServer.Models.RoleUser do |> Repo.insert end - def delete(role_id, user_id) do + def delete_user_role(role_id, user_id) do query = from ru in RoleUser, where: ru.role_id == ^role_id and ru.user_id == ^user_id Repo.delete_all(query) From 98aed623e7c8e750f59c78a25aa0c4491ec24184 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Fri, 16 Sep 2022 12:36:29 -1000 Subject: [PATCH 075/231] refactor(role-user): modify name of set_role->set_user_role, check before insert --- lib/epochtalk_server/models/role_user.ex | 13 +++++++------ lib/epochtalk_server/models/user.ex | 6 +----- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/lib/epochtalk_server/models/role_user.ex b/lib/epochtalk_server/models/role_user.ex index 9178542a..abff6a43 100644 --- a/lib/epochtalk_server/models/role_user.ex +++ b/lib/epochtalk_server/models/role_user.ex @@ -19,13 +19,14 @@ defmodule EpochtalkServer.Models.RoleUser do |> cast(attrs, [:user_id, :role_id]) |> validate_required([:user_id, :role_id]) end - def set_admin(%User{} = user), do: set_role(user, @admin_role_id) - def set_role(%User{} = user, role_id) do - %RoleUser{} - |> RoleUser.changeset(%{user_id: user.id, role_id: role_id}) - |> Repo.insert - end + def set_admin(user_id), do: set_user_role(@admin_role_id, user_id) + def set_user_role(role_id, user_id) do + case Repo.one(from(ru in RoleUser, where: ru.role_id == ^role_id and ru.user_id == ^user_id)) do + nil -> Repo.insert(changeset(%RoleUser{}, %{role_id: role_id, user_id: user_id})) + roleuser_cs -> {:ok, roleuser_cs} + end + end def delete_user_role(role_id, user_id) do query = from ru in RoleUser, where: ru.role_id == ^role_id and ru.user_id == ^user_id diff --git a/lib/epochtalk_server/models/user.ex b/lib/epochtalk_server/models/user.ex index 9daac119..a3dfc1c3 100644 --- a/lib/epochtalk_server/models/user.ex +++ b/lib/epochtalk_server/models/user.ex @@ -40,11 +40,7 @@ defmodule EpochtalkServer.Models.User do def create_user(user_attrs, true = _admin) do Repo.transaction(fn -> create_user(user_attrs) - |> case do - {:ok, user} -> - user - |> RoleUser.set_admin - end + |> case do {:ok, %{ id: id } = _user} -> RoleUser.set_admin(id) end end) end # create user, for seeding From 8ffbe259e48abcd8ecb5f809904bd00a8a927b5b Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Fri, 16 Sep 2022 12:53:00 -1000 Subject: [PATCH 076/231] feat(user-malicious-score): implement way to get and set malicious score for user, used by auth register --- lib/epochtalk_server/models/user.ex | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/epochtalk_server/models/user.ex b/lib/epochtalk_server/models/user.ex index a3dfc1c3..9ff4748b 100644 --- a/lib/epochtalk_server/models/user.ex +++ b/lib/epochtalk_server/models/user.ex @@ -8,6 +8,7 @@ defmodule EpochtalkServer.Models.User do alias EpochtalkServer.Models.Preference alias EpochtalkServer.Models.Role alias EpochtalkServer.Models.RoleUser + alias EpochtalkServer.Models.BannedAddress schema "users" do field :email, :string @@ -23,7 +24,7 @@ defmodule EpochtalkServer.Models.User do field :imported_at, :naive_datetime field :updated_at, :naive_datetime field :deleted, :boolean, default: false - field :malicious_score, :integer + field :malicious_score, :decimal field :smf_member, :map, virtual: true end @@ -53,9 +54,15 @@ defmodule EpochtalkServer.Models.User do def with_username_exists?(username), do: Repo.exists?(from u in User, where: u.username == ^username) def with_email_exists?(email), do: Repo.exists?(from u in User, where: u.email == ^email) def by_id(id) when is_integer(id), do: Repo.get_by(User, id: id) - def clear_malicious_score(id) when is_integer(id) do - from(u in User, where: u.id == ^id) - |> Repo.update_all(set: [malicious_score: nil]) + defp set_malicious_score(id, value) do + from(u in User, where: u.id == ^id) + |> Repo.update_all(set: [malicious_score: value]) + end + def clear_malicious_score(id) when is_integer(id), do: set_malicious_score(id, nil) + def get_and_set_malicious_score(id, ip) when is_integer(id) and is_binary(ip) do + malicious_score = BannedAddress.calculate_malicious_score_from_ip(ip) + set_malicious_score(id, malicious_score) + malicious_score end def by_username(username) when is_binary(username) do query = from u in User, From 24bd64c3cdedfc00e98551677effad469c283f7d Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 11:36:28 -1000 Subject: [PATCH 077/231] feat(user-seed): provide invalid argument reason on user seed --- priv/repo/seed_user.exs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/priv/repo/seed_user.exs b/priv/repo/seed_user.exs index b14ba33c..4701949d 100644 --- a/priv/repo/seed_user.exs +++ b/priv/repo/seed_user.exs @@ -8,8 +8,11 @@ System.argv() User.create_user(%{username: username, email: email, password: password}) _ -> IO.puts("Usage: mix seed.user [admin]") + {:error, "Invalid arguments"} end |> case do {:ok, _} -> IO.puts("Successfully seeded user") - {:error, _} -> IO.puts("Error seeding user") + {:error, error} -> + IO.puts("Error seeding user") + IO.inspect(error) end From b260d0fd2e952c5c4f1a1bd75ebbc61a7c0c6e18 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 12:17:14 -1000 Subject: [PATCH 078/231] feat(auth-view): get fields from user for credentials.json avatar, moderating, ban_expiration, malicious_score --- lib/epochtalk_server_web/views/auth_view.ex | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/epochtalk_server_web/views/auth_view.ex b/lib/epochtalk_server_web/views/auth_view.ex index 304e5387..a8ccaa42 100644 --- a/lib/epochtalk_server_web/views/auth_view.ex +++ b/lib/epochtalk_server_web/views/auth_view.ex @@ -27,10 +27,12 @@ defmodule EpochtalkServerWeb.AuthView do id: user.id, username: user.username, # TODO: fill in these fields - avatar: "", # user.avatar + avatar: Map.get(user, :avatar), # user.avatar permissions: %{}, # user.permissions - moderating: %{}, # user.moderating - roles: user.roles + moderating: Map.get(user, :moderating), # user.moderating + roles: user.roles, + ban_expiration: Map.get(user, :ban_expiration), + malicious_score: Map.get(user, :malicious_score) } end end From 359e2137736aec87800533865e7aefe861228851 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 12:18:35 -1000 Subject: [PATCH 079/231] fix(role): add @derive convert schema to json can use Map.get to get them out in view --- lib/epochtalk_server/models/role.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/epochtalk_server/models/role.ex b/lib/epochtalk_server/models/role.ex index 47e08362..60a420af 100644 --- a/lib/epochtalk_server/models/role.ex +++ b/lib/epochtalk_server/models/role.ex @@ -6,6 +6,7 @@ defmodule EpochtalkServer.Models.Role do alias EpochtalkServer.Models.Role alias EpochtalkServer.Models.RoleUser + @derive {Jason.Encoder, only: [:name, :description, :lookup, :priority, :highlight_color, :permissions, :priority_restrictions, :created_at, :updated_at]} schema "roles" do field :name, :string field :description, :string From 43bfa7e49333812aef9038e58f733a48ca262724 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 12:20:18 -1000 Subject: [PATCH 080/231] fix(session): fix typo in update ban info --- lib/epochtalk_server/session.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index e455f7ed..f2a8edcd 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -52,7 +52,7 @@ defmodule EpochtalkServer.Session do # save/replace ban_expiration to redis under "user:{user_id}:baninfo" ban_key = generate_key(user_id, "baninfo") Redix.command(:redix, ["HDEL", ban_key, "ban_expiration", "malicious_score"]) - Redix.command(:redix, ["HSET", ban_key, ban_info) + Redix.command(:redix, ["HSET", ban_key, ban_info]) end defp generate_key(user_id, "user"), do: "user:#{user_id}" defp generate_key(user_id, type), do: "user:#{user_id}:#{type}" From 50e7667154f3dc14779b7d6bde58ed252a851373 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 12:20:56 -1000 Subject: [PATCH 081/231] fix(session): use Map.get on db_user to get moderating --- lib/epochtalk_server/session.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index f2a8edcd..ebaab7ad 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -6,7 +6,7 @@ defmodule EpochtalkServer.Session do ban_info = if Map.has_key?(db_user, :ban_expiration), do: %{ ban_expiration: db_user.ban_expiration }, else: %{} ban_info = if Map.has_key?(db_user, :malicious_score), do: Map.put(ban_info, :malicious_score, db_user.malicious_score) update_ban_info(db_user.id, ban_info) - update_moderating(db_user.id, db_user.moderating) + update_moderating(db_user.id, Map.get(db_user, :moderating)) # TODO: do this outside of here, need guardian token # -- or don't do it if we don't need this functionality # // save user-session to redis set under "user:{user_id}:sessions" From d929efdb7c71da67f6cf086f7fdf132e432fc221 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 12:22:33 -1000 Subject: [PATCH 082/231] fix(session): enumerate over role_lookups in update_roles redix SADD doesn't handle lists --- lib/epochtalk_server/session.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index ebaab7ad..3ed4bfe4 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -23,7 +23,8 @@ defmodule EpochtalkServer.Session do |> Enum.map(&(&1.lookup)) role_key = generate_key(user_id, "roles") Redix.command(:redix, ["DEL", role_key]) - Redix.command(:redix, ["SADD", role_key, role_lookups]) + role_lookups + |> Enum.each(&Redix.command(:redix, ["SADD", role_key, &1])) end def update_moderating(user_id, moderating) do # save/replace moderating boards to redis under "user:{user_id}:moderating" From b78e4f55d9a49d737fd86cf0c587a2b3b28a3a66 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 12:24:54 -1000 Subject: [PATCH 083/231] fix(session): fix typos Redis -> Redix, :redis -> :redix (UndefinedFunctionError) function Redis.command/2 is undefined (module Redis is not available) --- lib/epochtalk_server/session.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index 3ed4bfe4..b8e5e724 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -37,7 +37,7 @@ defmodule EpochtalkServer.Session do def update_user_info(user_id, username) do user_key = generate_key(user_id, "user") # delete avatar from redis hash under "user:{user_id}" - Redis.command(:redis, ["HDEL", user_key, "avatar"]) + Redix.command(:redix, ["HDEL", user_key, "avatar"]) # save username to redis hash under "user:{user_id}" Redix.command(:redix, ["HSET", user_key, "username", db_user.username]) end From 9f079e89b008ddf45b6f2d5198148e761c74e5f5 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 12:32:29 -1000 Subject: [PATCH 084/231] fix(session): db_user.username/avatar -> username/avatar no longer passing db_user --- lib/epochtalk_server/session.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index b8e5e724..b027b38b 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -39,7 +39,7 @@ defmodule EpochtalkServer.Session do # delete avatar from redis hash under "user:{user_id}" Redix.command(:redix, ["HDEL", user_key, "avatar"]) # save username to redis hash under "user:{user_id}" - Redix.command(:redix, ["HSET", user_key, "username", db_user.username]) + Redix.command(:redix, ["HSET", user_key, "username", username]) end def update_user_info(user_id, username, avatar) when is_nil(avatar) or avatar == "" do update_user_info(user_id, username) @@ -47,7 +47,7 @@ defmodule EpochtalkServer.Session do def update_user_info(user_id, username, avatar) do # save username, avatar to redis hash under "user:{user_id}" user_key = generate_key(user_id, "user") - Redix.command(:redix, ["HSET", user_key, "username", db_user.username, "avatar", db_user.avatar]) + Redix.command(:redix, ["HSET", user_key, "username", username, "avatar", avatar]) end def update_ban_info(user_id, ban_info) do # save/replace ban_expiration to redis under "user:{user_id}:baninfo" From 1717a322df7226c8cdee96f3bc2c8d96e2ea47fd Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 12:34:57 -1000 Subject: [PATCH 085/231] refactor(auth-controller): use User.by_username in login instead of by_username_and_password need this to follow login flow in later steps --- lib/epochtalk_server_web/controllers/auth_controller.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index fbdbe178..498a485c 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -63,7 +63,7 @@ defmodule EpochtalkServerWeb.AuthController do login(conn, Map.put(user_params, "rememberMe", false)) end def login(conn, %{"username" => username, "password" => password} = user_params) do - if user = User.by_username_and_password(username, password) do + if user = User.by_username(username) do # TODO: check confirmation token # TODO: check ban expiration # TODO: get moderated boards From 2f6c3d9ee1adcbc54555336b5f92abf8fdfd4019 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 12:36:22 -1000 Subject: [PATCH 086/231] feat(auth-controller): use session.save on login this one's big! it's switching us into using the redis storage for session yay! --- lib/epochtalk_server_web/controllers/auth_controller.ex | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index 498a485c..e83a5cd8 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -3,6 +3,7 @@ defmodule EpochtalkServerWeb.AuthController do alias EpochtalkServer.Models.User alias EpochtalkServer.Repo alias EpochtalkServer.Auth.Guardian + alias EpochtalkServer.Session alias EpochtalkServerWeb.CustomErrors.{InvalidCredentials, NotLoggedIn} alias EpochtalkServerWeb.ErrorView @@ -90,10 +91,7 @@ defmodule EpochtalkServerWeb.AuthController do |> Guardian.Plug.current_token user = Map.put(user, :token, token) - - # TODO: check for empty roles first - # add default role - user = Map.put(user, :roles, ["user"]) + Session.save(user) conn |> render("credentials.json", user: user) From 61bb6620ea51fe00cb5923125c1925188aba45d0 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 12:40:35 -1000 Subject: [PATCH 087/231] feat(guardian): update resource from claims pull session data for user from redis instead of postgres --- lib/epochtalk_server/auth/guardian.ex | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/epochtalk_server/auth/guardian.ex b/lib/epochtalk_server/auth/guardian.ex index 9467cf0f..b5ab2575 100644 --- a/lib/epochtalk_server/auth/guardian.ex +++ b/lib/epochtalk_server/auth/guardian.ex @@ -19,10 +19,14 @@ defmodule EpochtalkServer.Auth.Guardian do # Here we'll look up our resource from the claims, the subject can be # found in the `"sub"` key. In above `subject_for_token/2` we returned # the resource id so here we'll rely on that to look it up. - resource = user_id - |> String.to_integer - # TODO: redis call here instead - |> User.by_id + resource = %{ + id: user_id, + # session_id: ?, + username: Redix.command(:redix, ["HGET", "user:#{user_id}", "username"]), + avatar: Redix.command(:redix, ["HGET", "user:#{user_id}", "avatar"]), + roles: Redix.command(:redix, ["GET", "user:#{user_id}:roles"]), + moderating: Redix.command(:redix, ["GET", "user:#{user_id}:moderating"]) + } {:ok, resource} end def resource_from_claims(_claims) do From 83f630044d818df4edcff0e10f42ffcb050a8244 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 12:55:32 -1000 Subject: [PATCH 088/231] refactor(auth-controller): don't add default role "user" on authenticate --- lib/epochtalk_server_web/controllers/auth_controller.ex | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index e83a5cd8..19f86180 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -43,10 +43,6 @@ defmodule EpochtalkServerWeb.AuthController do user = Map.put(user, :token, token) - # TODO: check for empty roles first - # add default role - user = Map.put(user, :roles, ["user"]) - conn |> render("credentials.json", user: user) end From e6f320aa28eabe8f176ced4fdbc34adb118f00c2 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 13:00:23 -1000 Subject: [PATCH 089/231] refactor(guardian): use bang methods to retrieve from redis error handling implementation tbd --- lib/epochtalk_server/auth/guardian.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/epochtalk_server/auth/guardian.ex b/lib/epochtalk_server/auth/guardian.ex index b5ab2575..6328a49e 100644 --- a/lib/epochtalk_server/auth/guardian.ex +++ b/lib/epochtalk_server/auth/guardian.ex @@ -22,10 +22,10 @@ defmodule EpochtalkServer.Auth.Guardian do resource = %{ id: user_id, # session_id: ?, - username: Redix.command(:redix, ["HGET", "user:#{user_id}", "username"]), - avatar: Redix.command(:redix, ["HGET", "user:#{user_id}", "avatar"]), - roles: Redix.command(:redix, ["GET", "user:#{user_id}:roles"]), - moderating: Redix.command(:redix, ["GET", "user:#{user_id}:moderating"]) + username: Redix.command!(:redix, ["HGET", "user:#{user_id}", "username"]), + avatar: Redix.command!(:redix, ["HGET", "user:#{user_id}", "avatar"]), + roles: Redix.command!(:redix, ["SMEMBERS", "user:#{user_id}:roles"]), + moderating: Redix.command!(:redix, ["GET", "user:#{user_id}:moderating"]) } {:ok, resource} end From 449bc91e8e209ce69f31a10090e41828a4860d0c Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 14:03:32 -1000 Subject: [PATCH 090/231] feat(guardian): retrieve ban_expiration and malicious_score on auth --- lib/epochtalk_server/auth/guardian.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/epochtalk_server/auth/guardian.ex b/lib/epochtalk_server/auth/guardian.ex index 6328a49e..93342e7d 100644 --- a/lib/epochtalk_server/auth/guardian.ex +++ b/lib/epochtalk_server/auth/guardian.ex @@ -25,7 +25,9 @@ defmodule EpochtalkServer.Auth.Guardian do username: Redix.command!(:redix, ["HGET", "user:#{user_id}", "username"]), avatar: Redix.command!(:redix, ["HGET", "user:#{user_id}", "avatar"]), roles: Redix.command!(:redix, ["SMEMBERS", "user:#{user_id}:roles"]), - moderating: Redix.command!(:redix, ["GET", "user:#{user_id}:moderating"]) + moderating: Redix.command!(:redix, ["GET", "user:#{user_id}:moderating"]), + ban_expiration: Redix.command!(:redix, ["HEXISTS", "user:#{user_id}:ban_info", "ban_expiration"]), + malicious_score: Redix.command!(:redix, ["HEXISTS", "user:#{user_id}:ban_info", "malicious_score"]) } {:ok, resource} end From 26d97a849b5c90eaa36a315791ead99eba70b0db Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 14:04:20 -1000 Subject: [PATCH 091/231] refactor(router): don't use ensure not auth on login --- lib/epochtalk_server_web/router.ex | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/epochtalk_server_web/router.ex b/lib/epochtalk_server_web/router.ex index 0bc7fad7..bc832127 100644 --- a/lib/epochtalk_server_web/router.ex +++ b/lib/epochtalk_server_web/router.ex @@ -14,23 +14,17 @@ defmodule EpochtalkServerWeb.Router do pipeline :enforce_auth do plug Guardian.Plug.EnsureAuthenticated end - pipeline :enforce_not_auth do - plug Guardian.Plug.EnsureNotAuthenticated - end scope "/api", EpochtalkServerWeb do pipe_through [:api, :maybe_auth, :enforce_auth] get "/authenticate", AuthController, :authenticate end - scope "/api", EpochtalkServerWeb do - pipe_through [:api, :maybe_auth, :enforce_not_auth] - post "/login", AuthController, :login - end scope "/api", EpochtalkServerWeb do pipe_through [:api, :maybe_auth] get "/register/username/:username", AuthController, :username get "/register/email/:email", AuthController, :email post "/register", AuthController, :register + post "/login", AuthController, :login delete "/logout", AuthController, :logout end From 1149839343e5af9aa6f1aacda1aadd590d25d253 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 14:21:05 -1000 Subject: [PATCH 092/231] refactor(errors): add auth prefix to all auth-related errors --- lib/epochtalk_server_web/views/custom_errors.ex | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server_web/views/custom_errors.ex b/lib/epochtalk_server_web/views/custom_errors.ex index a3e5f580..6c92489d 100644 --- a/lib/epochtalk_server_web/views/custom_errors.ex +++ b/lib/epochtalk_server_web/views/custom_errors.ex @@ -1,14 +1,17 @@ defmodule EpochtalkServerWeb.CustomErrors do + @auth_prefix "Auth:" + + # Auth errors defmodule InvalidCredentials do @moduledoc """ Exception raised when user and password are not correct """ - defexception plug_status: 400, message: "Invalid credentials", conn: nil, router: nil + defexception plug_status: 400, message: "#{auth_prefix} Invalid credentials", conn: nil, router: nil end defmodule NotLoggedIn do @moduledoc """ Exception raised when user is not logged in """ - defexception plug_status: 400, message: "Not logged in", conn: nil, router: nil + defexception plug_status: 400, message: "#{auth_prefix} Not logged in", conn: nil, router: nil end end From ef25441c08c11d07a5e259f84d1b9b6fddc99a6f Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 14:21:27 -1000 Subject: [PATCH 093/231] feat(errors): account not confirmed error --- lib/epochtalk_server_web/views/custom_errors.ex | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/epochtalk_server_web/views/custom_errors.ex b/lib/epochtalk_server_web/views/custom_errors.ex index 6c92489d..5ce34bc5 100644 --- a/lib/epochtalk_server_web/views/custom_errors.ex +++ b/lib/epochtalk_server_web/views/custom_errors.ex @@ -14,4 +14,10 @@ defmodule EpochtalkServerWeb.CustomErrors do """ defexception plug_status: 400, message: "#{auth_prefix} Not logged in", conn: nil, router: nil end + defmodule AccountNotConfirmed do + @moduledoc """ + Exception raised when user's account is not confirmed + """ + defexception plug_status: 400, message: "#{auth_prefix} User account not confirmed", conn: nil, router: nil + end end From a4f7cedf44948ff84084255a26b84fbc1ae40426 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 14:22:14 -1000 Subject: [PATCH 094/231] feat(auth-controller): alias account not confirmed error --- lib/epochtalk_server_web/controllers/auth_controller.ex | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index 19f86180..35eb0fd8 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -5,7 +5,11 @@ defmodule EpochtalkServerWeb.AuthController do alias EpochtalkServer.Auth.Guardian alias EpochtalkServer.Session - alias EpochtalkServerWeb.CustomErrors.{InvalidCredentials, NotLoggedIn} + alias EpochtalkServerWeb.CustomErrors.{ + InvalidCredentials, + NotLoggedIn, + AccountNotConfirmed + } alias EpochtalkServerWeb.ErrorView def username(conn, %{"username" => username}) do From d0d59d951d393777490f2e851bf8eb9495d1273f Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 14:23:53 -1000 Subject: [PATCH 095/231] feat(auth-controller): wip, stub replacement login check if user exists use AccountNotConfirmed when checking confirmation token --- .../controllers/auth_controller.ex | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index 35eb0fd8..8982f686 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -64,6 +64,20 @@ defmodule EpochtalkServerWeb.AuthController do login(conn, Map.put(user_params, "rememberMe", false)) end def login(conn, %{"username" => username, "password" => password} = user_params) do + user = username + |> User.by_username + # check that user exists + |> case do + nil -> raise(InvalidCredentials) + user -> + # check confirmation token + Map.get(user, :confirmation_token) + |> case do + nil -> user + confirmation_token -> raise(AccountNotConfirmed) + end + end + if user = User.by_username(username) do # TODO: check confirmation token # TODO: check ban expiration From 1c9f2e6da27c103cd82c59c7da430fac4af9121d Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 14:33:02 -1000 Subject: [PATCH 096/231] feat(errors): implement AccountMigrationNotComplete error --- lib/epochtalk_server_web/views/custom_errors.ex | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/epochtalk_server_web/views/custom_errors.ex b/lib/epochtalk_server_web/views/custom_errors.ex index 5ce34bc5..738787d3 100644 --- a/lib/epochtalk_server_web/views/custom_errors.ex +++ b/lib/epochtalk_server_web/views/custom_errors.ex @@ -20,4 +20,10 @@ defmodule EpochtalkServerWeb.CustomErrors do """ defexception plug_status: 400, message: "#{auth_prefix} User account not confirmed", conn: nil, router: nil end + defmodule AccountMigrationNotComplete do + @moduledoc """ + Exception raised when user's account is not fully migrated + """ + defexception plug_status: 403, message: "#{auth_prefix} User account migration not complete, please reset password", conn: nil, router: nil + end end From 13a0571758659010fa5d5192af448f79a77566bb Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 14:38:03 -1000 Subject: [PATCH 097/231] feat(auth-controller): check user migration on login use AcccountMigrationNotComplete error if migration is not done --- .../controllers/auth_controller.ex | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index 8982f686..c1b94552 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -8,7 +8,8 @@ defmodule EpochtalkServerWeb.AuthController do alias EpochtalkServerWeb.CustomErrors.{ InvalidCredentials, NotLoggedIn, - AccountNotConfirmed + AccountNotConfirmed, + AcccountMigrationNotComplete } alias EpochtalkServerWeb.ErrorView @@ -76,6 +77,13 @@ defmodule EpochtalkServerWeb.AuthController do nil -> user confirmation_token -> raise(AccountNotConfirmed) end + # check user migration + # if passhash doesn't exist, account hasn't been fully migrated + |> Map.get(:passhash) + |> case do + nil -> raise(AcccountMigrationNotComplete) + _passhash -> user + end end if user = User.by_username(username) do From 5f9bdb5e2d67970e1f766ba0fb6bfe2eae9ea821 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 14:38:59 -1000 Subject: [PATCH 098/231] refactor(auth-controller): remove unnecessary variable user --- lib/epochtalk_server_web/controllers/auth_controller.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index c1b94552..459e10fb 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -65,7 +65,7 @@ defmodule EpochtalkServerWeb.AuthController do login(conn, Map.put(user_params, "rememberMe", false)) end def login(conn, %{"username" => username, "password" => password} = user_params) do - user = username + username |> User.by_username # check that user exists |> case do From bd2b9fd86bc0b437943dc4c460df77d488718dd0 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 14:39:28 -1000 Subject: [PATCH 099/231] refactor(auth-controller): pipe --- lib/epochtalk_server_web/controllers/auth_controller.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index 459e10fb..828183b2 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -72,7 +72,8 @@ defmodule EpochtalkServerWeb.AuthController do nil -> raise(InvalidCredentials) user -> # check confirmation token - Map.get(user, :confirmation_token) + user + |> Map.get(:confirmation_token) |> case do nil -> user confirmation_token -> raise(AccountNotConfirmed) From 8b5d81b487facaa3b7c378b921178d402ce68712 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 14:39:44 -1000 Subject: [PATCH 100/231] refactor(auth-controller): confirmation token underscore variable not used --- lib/epochtalk_server_web/controllers/auth_controller.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index 828183b2..872a3e93 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -76,7 +76,7 @@ defmodule EpochtalkServerWeb.AuthController do |> Map.get(:confirmation_token) |> case do nil -> user - confirmation_token -> raise(AccountNotConfirmed) + _confirmation_token -> raise(AccountNotConfirmed) end # check user migration # if passhash doesn't exist, account hasn't been fully migrated From 638a333449458841b26e0bd1e04e46d7754b394e Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 14:41:11 -1000 Subject: [PATCH 101/231] refactor(auth-controller): remove confirmation token comment moved to confirmation token check --- lib/epochtalk_server_web/controllers/auth_controller.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index 872a3e93..b73acb08 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -88,7 +88,6 @@ defmodule EpochtalkServerWeb.AuthController do end if user = User.by_username(username) do - # TODO: check confirmation token # TODO: check ban expiration # TODO: get moderated boards log_in_user(conn, user, user_params) From 7330e0ba90dce703c75b98a079507736bf926909 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 14:46:18 -1000 Subject: [PATCH 102/231] refactor(auth-controller): un-nest login checks --- .../controllers/auth_controller.ex | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index b73acb08..01f9e693 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -65,26 +65,29 @@ defmodule EpochtalkServerWeb.AuthController do login(conn, Map.put(user_params, "rememberMe", false)) end def login(conn, %{"username" => username, "password" => password} = user_params) do - username + user = username |> User.by_username # check that user exists |> case do nil -> raise(InvalidCredentials) - user -> - # check confirmation token - user - |> Map.get(:confirmation_token) - |> case do - nil -> user - _confirmation_token -> raise(AccountNotConfirmed) - end - # check user migration - # if passhash doesn't exist, account hasn't been fully migrated - |> Map.get(:passhash) - |> case do - nil -> raise(AcccountMigrationNotComplete) - _passhash -> user - end + user -> user + end + + # check confirmation token + user + |> Map.get(:confirmation_token) + |> case do + nil -> user + _confirmation_token -> raise(AccountNotConfirmed) + end + + # check user migration + # if passhash doesn't exist, account hasn't been fully migrated + user + |> Map.get(:passhash) + |> case do + nil -> raise(AcccountMigrationNotComplete) + _passhash -> user end if user = User.by_username(username) do From 802d842d6a8105ba09b047a57cd70b096d8e7589 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 14:51:06 -1000 Subject: [PATCH 103/231] fix(auth-controller): typo, AcccountMigrationNotComplete -> AccountMigrationNotComplete --- lib/epochtalk_server_web/controllers/auth_controller.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index 01f9e693..74d0680d 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -9,7 +9,7 @@ defmodule EpochtalkServerWeb.AuthController do InvalidCredentials, NotLoggedIn, AccountNotConfirmed, - AcccountMigrationNotComplete + AccountMigrationNotComplete } alias EpochtalkServerWeb.ErrorView @@ -86,7 +86,7 @@ defmodule EpochtalkServerWeb.AuthController do user |> Map.get(:passhash) |> case do - nil -> raise(AcccountMigrationNotComplete) + nil -> raise(AccountMigrationNotComplete) _passhash -> user end From 0e91bc646ffc67d3e5cd9cb99c317e6b77295c87 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 14:51:48 -1000 Subject: [PATCH 104/231] refactor(auth-controller): AccountNotConfirmed flow --- lib/epochtalk_server_web/controllers/auth_controller.ex | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index 74d0680d..174ce86f 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -74,12 +74,7 @@ defmodule EpochtalkServerWeb.AuthController do end # check confirmation token - user - |> Map.get(:confirmation_token) - |> case do - nil -> user - _confirmation_token -> raise(AccountNotConfirmed) - end + if Map.get(user, :confirmation_token) != nil, do: raise(AccountNotConfirmed) # check user migration # if passhash doesn't exist, account hasn't been fully migrated From 34cbac17bf6072cd86f788e3cb1ddb9bbd04ba60 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 14:55:41 -1000 Subject: [PATCH 105/231] refactor(auth-controller): user exists check, use if --- lib/epochtalk_server_web/controllers/auth_controller.ex | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index 174ce86f..cf5b94c8 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -67,11 +67,9 @@ defmodule EpochtalkServerWeb.AuthController do def login(conn, %{"username" => username, "password" => password} = user_params) do user = username |> User.by_username + # check that user exists - |> case do - nil -> raise(InvalidCredentials) - user -> user - end + if !user, do: raise(InvalidCredentials) # check confirmation token if Map.get(user, :confirmation_token) != nil, do: raise(AccountNotConfirmed) From 692d93d2444e589583d2e3c7f218c6cb75472b67 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 14:56:12 -1000 Subject: [PATCH 106/231] refactor(auth-controller): confirmation check, don't check nil --- lib/epochtalk_server_web/controllers/auth_controller.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index cf5b94c8..b2155250 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -72,7 +72,7 @@ defmodule EpochtalkServerWeb.AuthController do if !user, do: raise(InvalidCredentials) # check confirmation token - if Map.get(user, :confirmation_token) != nil, do: raise(AccountNotConfirmed) + if Map.get(user, :confirmation_token), do: raise(AccountNotConfirmed) # check user migration # if passhash doesn't exist, account hasn't been fully migrated From 63ce00772c6597fc15a9962bcfe3899c8e47e2c8 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 14:56:53 -1000 Subject: [PATCH 107/231] refactor(auth-controller): user migration check, use if --- lib/epochtalk_server_web/controllers/auth_controller.ex | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index b2155250..fb346003 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -76,12 +76,7 @@ defmodule EpochtalkServerWeb.AuthController do # check user migration # if passhash doesn't exist, account hasn't been fully migrated - user - |> Map.get(:passhash) - |> case do - nil -> raise(AccountMigrationNotComplete) - _passhash -> user - end + if !Map.get(user, :passhash), do: raise(AccountMigrationNotComplete) if user = User.by_username(username) do # TODO: check ban expiration From a9930da320210d4d8bf5017163ed219e9c53913e Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 15:03:43 -1000 Subject: [PATCH 108/231] feat(auth-controller): check for valid user password --- lib/epochtalk_server_web/controllers/auth_controller.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index fb346003..f2d7a1dd 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -78,6 +78,7 @@ defmodule EpochtalkServerWeb.AuthController do # if passhash doesn't exist, account hasn't been fully migrated if !Map.get(user, :passhash), do: raise(AccountMigrationNotComplete) + if !User.valid_password?(user, password), do: raise(InvalidCredentials) if user = User.by_username(username) do # TODO: check ban expiration # TODO: get moderated boards From 3b2fbccfbdd5c2e6e013a4ac114ebd792570b9fc Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 15:04:08 -1000 Subject: [PATCH 109/231] docs(auth-controller): comment explaining user password check --- lib/epochtalk_server_web/controllers/auth_controller.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index f2d7a1dd..af784e8f 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -78,6 +78,7 @@ defmodule EpochtalkServerWeb.AuthController do # if passhash doesn't exist, account hasn't been fully migrated if !Map.get(user, :passhash), do: raise(AccountMigrationNotComplete) + # check password if !User.valid_password?(user, password), do: raise(InvalidCredentials) if user = User.by_username(username) do # TODO: check ban expiration From f43da2e22419f52b8157221c8f212c691bb74c08 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 15:04:29 -1000 Subject: [PATCH 110/231] style(auth-controller): add whitespace --- lib/epochtalk_server_web/controllers/auth_controller.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index af784e8f..a1e3c9f8 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -80,6 +80,7 @@ defmodule EpochtalkServerWeb.AuthController do # check password if !User.valid_password?(user, password), do: raise(InvalidCredentials) + if user = User.by_username(username) do # TODO: check ban expiration # TODO: get moderated boards From a2314e098b2069a8aa5e2146714ae18e830ba9c4 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 19 Sep 2022 15:07:37 -1000 Subject: [PATCH 111/231] feat(bans): implement user ban --- lib/epochtalk_server/models/ban.ex | 39 ++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/lib/epochtalk_server/models/ban.ex b/lib/epochtalk_server/models/ban.ex index 6729144a..65a61437 100644 --- a/lib/epochtalk_server/models/ban.ex +++ b/lib/epochtalk_server/models/ban.ex @@ -7,7 +7,11 @@ defmodule EpochtalkServer.Models.Ban do alias EpochtalkServer.Models.RoleUser alias EpochtalkServer.Models.Ban + # max naivedatetime, used for permanent bans + @max_date ~N[9999-12-31 00:00:00.000] + @schema_prefix "users" + @derive {Jason.Encoder, only: [:user_id, :expiration, :created_at, :updated_at]} schema "bans" do belongs_to :user, User, primary_key: true field :expiration, :naive_datetime @@ -21,6 +25,19 @@ defmodule EpochtalkServer.Models.Ban do |> validate_required([:user_id]) end + # handles upsert of ban for banning + def ban_changeset(ban, attrs \\ %{}) do + now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) + attrs = attrs + |> Map.put(:created_at, (if created_at = Map.get(ban, :created_at), do: created_at, else: now)) + |> Map.put(:updated_at, now) + |> Map.put(:expiration, (if exp = Map.get(attrs, :expiration), do: exp, else: @max_date)) + ban + |> cast(attrs, [:id, :user_id, :expiration, :updated_at, :created_at]) + |> validate_required([:user_id]) + end + + # handles update of ban for unbanning def unban_changeset(ban, attrs \\ %{}) do now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) attrs = attrs @@ -30,17 +47,29 @@ defmodule EpochtalkServer.Models.Ban do |> cast(attrs, [:user_id, :expiration, :updated_at]) |> validate_required([:user_id]) end + def by_user_id(user_id) when is_integer(user_id), do: Repo.get_by(Ban, user_id: user_id) + + def ban(user_id, expiration) do + Repo.transaction(fn -> + RoleUser.set_user_role(Role.get_banned_role_id, user_id) + case Repo.get_by(Ban, user_id: user_id) do + nil -> ban_changeset(%Ban{}, %{user_id: user_id, expiration: expiration}) + ban -> ban_changeset(ban, %{user_id: user_id, expiration: expiration}) + end + |> Repo.insert_or_update! + |> Map.put(:roles, Role.by_user_id(user_id)) + end) + end - def insert(%Ban{} = ban), do: Repo.insert(ban) def unban(user_id) when is_integer(user_id) do Repo.transaction(fn -> - db_ban = case Repo.get_by(Ban, user_id: user_id) do + RoleUser.delete_user_role(Role.get_banned_role_id, user_id) # delete ban role from user + User.clear_malicious_score(user_id) # clear user malicious score + case Repo.get_by(Ban, user_id: user_id) do nil -> %{ user_id: user_id } cs -> Repo.update!(unban_changeset(cs, %{ user_id: user_id })) end # unban the user - User.clear_malicious_score(user_id) # clear user malicious score - RoleUser.delete(Role.get_banned_role_id, user_id) # delete ban role from user - Map.put(db_ban, :roles, Role.by_user_id(user_id)) # append user roles + |> Map.put(:roles, Role.by_user_id(user_id)) # append user roles end) end end From 060dfb5d28e385dabf8ebf45f8f5dd951e32c86e Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 15:20:09 -1000 Subject: [PATCH 112/231] fix(errors): oops, auth_prefix -> @auth_prefix --- lib/epochtalk_server_web/views/custom_errors.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/epochtalk_server_web/views/custom_errors.ex b/lib/epochtalk_server_web/views/custom_errors.ex index 738787d3..be25c83a 100644 --- a/lib/epochtalk_server_web/views/custom_errors.ex +++ b/lib/epochtalk_server_web/views/custom_errors.ex @@ -6,24 +6,24 @@ defmodule EpochtalkServerWeb.CustomErrors do @moduledoc """ Exception raised when user and password are not correct """ - defexception plug_status: 400, message: "#{auth_prefix} Invalid credentials", conn: nil, router: nil + defexception plug_status: 400, message: "#{@auth_prefix} Invalid credentials", conn: nil, router: nil end defmodule NotLoggedIn do @moduledoc """ Exception raised when user is not logged in """ - defexception plug_status: 400, message: "#{auth_prefix} Not logged in", conn: nil, router: nil + defexception plug_status: 400, message: "#{@auth_prefix} Not logged in", conn: nil, router: nil end defmodule AccountNotConfirmed do @moduledoc """ Exception raised when user's account is not confirmed """ - defexception plug_status: 400, message: "#{auth_prefix} User account not confirmed", conn: nil, router: nil + defexception plug_status: 400, message: "#{@auth_prefix} User account not confirmed", conn: nil, router: nil end defmodule AccountMigrationNotComplete do @moduledoc """ Exception raised when user's account is not fully migrated """ - defexception plug_status: 403, message: "#{auth_prefix} User account migration not complete, please reset password", conn: nil, router: nil + defexception plug_status: 403, message: "#{@auth_prefix} User account migration not complete, please reset password", conn: nil, router: nil end end From 69face8b67cc42aa2b9b7c59cc82e8a5654f17af Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 15:27:14 -1000 Subject: [PATCH 113/231] fix(errors): don't use module attribute for auth prefix oops, i did it again --- lib/epochtalk_server_web/views/custom_errors.ex | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/epochtalk_server_web/views/custom_errors.ex b/lib/epochtalk_server_web/views/custom_errors.ex index be25c83a..b6c5f801 100644 --- a/lib/epochtalk_server_web/views/custom_errors.ex +++ b/lib/epochtalk_server_web/views/custom_errors.ex @@ -1,29 +1,29 @@ defmodule EpochtalkServerWeb.CustomErrors do - @auth_prefix "Auth:" + auth_prefix = "Auth:" # Auth errors defmodule InvalidCredentials do @moduledoc """ Exception raised when user and password are not correct """ - defexception plug_status: 400, message: "#{@auth_prefix} Invalid credentials", conn: nil, router: nil + defexception plug_status: 400, message: "#{auth_prefix} Invalid credentials", conn: nil, router: nil end defmodule NotLoggedIn do @moduledoc """ Exception raised when user is not logged in """ - defexception plug_status: 400, message: "#{@auth_prefix} Not logged in", conn: nil, router: nil + defexception plug_status: 400, message: "#{auth_prefix} Not logged in", conn: nil, router: nil end defmodule AccountNotConfirmed do @moduledoc """ Exception raised when user's account is not confirmed """ - defexception plug_status: 400, message: "#{@auth_prefix} User account not confirmed", conn: nil, router: nil + defexception plug_status: 400, message: "#{auth_prefix} User account not confirmed", conn: nil, router: nil end defmodule AccountMigrationNotComplete do @moduledoc """ Exception raised when user's account is not fully migrated """ - defexception plug_status: 403, message: "#{@auth_prefix} User account migration not complete, please reset password", conn: nil, router: nil + defexception plug_status: 403, message: "#{auth_prefix} User account migration not complete, please reset password", conn: nil, router: nil end end From 16ce2ebf08431885ecf6a78f7d6c7d809ee88e34 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 19 Sep 2022 15:39:03 -1000 Subject: [PATCH 114/231] refactor(model-plurality): fix model plurality for board moderators and metadata boards, remove plural --- lib/epochtalk_server/models/board.ex | 4 ++-- .../models/{board_moderators.ex => board_moderator.ex} | 6 +++--- .../models/{metadata_boards.ex => metadata_board.ex} | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) rename lib/epochtalk_server/models/{board_moderators.ex => board_moderator.ex} (76%) rename lib/epochtalk_server/models/{metadata_boards.ex => metadata_board.ex} (82%) diff --git a/lib/epochtalk_server/models/board.ex b/lib/epochtalk_server/models/board.ex index c12ed7f8..b0c6975a 100644 --- a/lib/epochtalk_server/models/board.ex +++ b/lib/epochtalk_server/models/board.ex @@ -5,7 +5,7 @@ defmodule EpochtalkServer.Models.Board do alias EpochtalkServer.Models.Category alias EpochtalkServer.Models.BoardMapping alias EpochtalkServer.Models.Board - alias EpochtalkServer.Models.MetadataBoards + alias EpochtalkServer.Models.MetadataBoard schema "boards" do field :name, :string @@ -44,7 +44,7 @@ defmodule EpochtalkServer.Models.Board do def create(board) do board_cs = create_changeset(%Board{}, board) case Repo.insert(board_cs) do - {:ok, db_board} -> case MetadataBoards.insert(%MetadataBoards{ board_id: db_board.id}) do + {:ok, db_board} -> case MetadataBoard.insert(%MetadataBoard{ board_id: db_board.id}) do {:ok, _} -> db_board {:error, cs} -> {:error, cs} end diff --git a/lib/epochtalk_server/models/board_moderators.ex b/lib/epochtalk_server/models/board_moderator.ex similarity index 76% rename from lib/epochtalk_server/models/board_moderators.ex rename to lib/epochtalk_server/models/board_moderator.ex index 748fc77c..feb01e3f 100644 --- a/lib/epochtalk_server/models/board_moderators.ex +++ b/lib/epochtalk_server/models/board_moderator.ex @@ -1,10 +1,10 @@ -defmodule EpochtalkServer.Models.BoardModerators do +defmodule EpochtalkServer.Models.BoardModerator do use Ecto.Schema import Ecto.Changeset alias EpochtalkServer.Repo alias EpochtalkServer.Models.User alias EpochtalkServer.Models.Board - alias EpochtalkServer.Models.BoardModerators + alias EpochtalkServer.Models.BoardModerator @primary_key false schema "board_moderators" do @@ -19,6 +19,6 @@ defmodule EpochtalkServer.Models.BoardModerators do end def get_boards(user_id) when is_integer(user_id) do - Repo.get_by(BoardModerators, user_id: user_id) + Repo.get_by(BoardModerator, user_id: user_id) end end diff --git a/lib/epochtalk_server/models/metadata_boards.ex b/lib/epochtalk_server/models/metadata_board.ex similarity index 82% rename from lib/epochtalk_server/models/metadata_boards.ex rename to lib/epochtalk_server/models/metadata_board.ex index c11e116a..c1f1f5c8 100644 --- a/lib/epochtalk_server/models/metadata_boards.ex +++ b/lib/epochtalk_server/models/metadata_board.ex @@ -1,9 +1,9 @@ -defmodule EpochtalkServer.Models.MetadataBoards do +defmodule EpochtalkServer.Models.MetadataBoard do use Ecto.Schema import Ecto.Changeset alias EpochtalkServer.Repo alias EpochtalkServer.Models.Board - alias EpochtalkServer.Models.MetadataBoards + alias EpochtalkServer.Models.MetadataBoard @schema_prefix "metadata" schema "boards" do @@ -27,5 +27,5 @@ defmodule EpochtalkServer.Models.MetadataBoards do :last_post_position]) |> validate_required([:board_id]) end - def insert(%MetadataBoards{} = metadata_boards), do: Repo.insert(metadata_boards) + def insert(%MetadataBoard{} = metadata_boards), do: Repo.insert(metadata_boards) end From 31d026a46ccf8952fa81fa821a3392e542825300 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 15:44:13 -1000 Subject: [PATCH 115/231] chore(.iex.exs): implement .iex alias for all models reload functionality --- .iex.exs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .iex.exs diff --git a/.iex.exs b/.iex.exs new file mode 100644 index 00000000..97a0c30b --- /dev/null +++ b/.iex.exs @@ -0,0 +1,37 @@ +alias EpochtalkServer.Repo + +alias EpochtalkServer.Models.Ban +alias EpochtalkServer.Models.BannedAddress +alias EpochtalkServer.Models.Board +alias EpochtalkServer.Models.BoardMapping +alias EpochtalkServer.Models.BoardModerator +alias EpochtalkServer.Models.Category +alias EpochtalkServer.Models.Invitation +alias EpochtalkServer.Models.MetadataBoard +alias EpochtalkServer.Models.Permission +alias EpochtalkServer.Models.Preference +alias EpochtalkServer.Models.Profile +alias EpochtalkServer.Models.Role +alias EpochtalkServer.Models.RolePermission +alias EpochtalkServer.Models.RoleUser +alias EpochtalkServer.Models.User + +reload = fn() -> + r [ + Ban, + BannedAddress, + Board, + BoardMapping, + BoardModerator, + Category, + Invitation, + MetadataBoard, + Permission, + Preference, + Profile, + Role, + RolePermission, + RoleUser, + User + ] +end From 0b9a7da0f54d30ab86367dec8364323adb3b68ac Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 15:57:43 -1000 Subject: [PATCH 116/231] chore(.iex.exs): refactor .iex, reload via __ENV__ --- .iex.exs | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/.iex.exs b/.iex.exs index 97a0c30b..3e7c643e 100644 --- a/.iex.exs +++ b/.iex.exs @@ -16,22 +16,4 @@ alias EpochtalkServer.Models.RolePermission alias EpochtalkServer.Models.RoleUser alias EpochtalkServer.Models.User -reload = fn() -> - r [ - Ban, - BannedAddress, - Board, - BoardMapping, - BoardModerator, - Category, - Invitation, - MetadataBoard, - Permission, - Preference, - Profile, - Role, - RolePermission, - RoleUser, - User - ] -end +reload = fn() -> r Enum.map(__ENV__.aliases, fn {_, module} -> module end) end From 4a10212146b23121b12de97df7712c24095d6e93 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 16:14:37 -1000 Subject: [PATCH 117/231] feat(ban): implement unban_if_expired --- lib/epochtalk_server/models/ban.ex | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/epochtalk_server/models/ban.ex b/lib/epochtalk_server/models/ban.ex index 65a61437..d49fe594 100644 --- a/lib/epochtalk_server/models/ban.ex +++ b/lib/epochtalk_server/models/ban.ex @@ -72,4 +72,9 @@ defmodule EpochtalkServer.Models.Ban do |> Map.put(:roles, Role.by_user_id(user_id)) # append user roles end) end + + def unban_if_expired(%{ban_expiration: expiration, id: user_id} = _user) do + if NaiveDateTime.compare(expiration, NaiveDateTime.utc_now) == :lt, do: unban(user_id) + end + def unban_if_expired(user), do: user end From 575023569e36085c262917e6ccc1ce9761a90492 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 16:28:50 -1000 Subject: [PATCH 118/231] feat(auth-controller): unban_if_expired on login, update roles --- lib/epochtalk_server/models/ban.ex | 5 +++-- lib/epochtalk_server_web/controllers/auth_controller.ex | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server/models/ban.ex b/lib/epochtalk_server/models/ban.ex index d49fe594..c386c002 100644 --- a/lib/epochtalk_server/models/ban.ex +++ b/lib/epochtalk_server/models/ban.ex @@ -73,8 +73,9 @@ defmodule EpochtalkServer.Models.Ban do end) end + # unban if ban is expired, return user's roles def unban_if_expired(%{ban_expiration: expiration, id: user_id} = _user) do - if NaiveDateTime.compare(expiration, NaiveDateTime.utc_now) == :lt, do: unban(user_id) + if NaiveDateTime.compare(expiration, NaiveDateTime.utc_now) == :lt, do: unban(user_id) |> Map.get(:roles) end - def unban_if_expired(user), do: user + def unban_if_expired(user), do: user.roles end diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index a1e3c9f8..c4a11f47 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -1,6 +1,7 @@ defmodule EpochtalkServerWeb.AuthController do use EpochtalkServerWeb, :controller alias EpochtalkServer.Models.User + alias EpochtalkServer.Models.Ban alias EpochtalkServer.Repo alias EpochtalkServer.Auth.Guardian alias EpochtalkServer.Session @@ -81,6 +82,9 @@ defmodule EpochtalkServerWeb.AuthController do # check password if !User.valid_password?(user, password), do: raise(InvalidCredentials) + # unban if ban is expired and update roles + user = Map.put(user, :roles, Ban.unban_if_expired(user)) + if user = User.by_username(username) do # TODO: check ban expiration # TODO: get moderated boards From 48a9ddaea2750be79bbe79a2857424ee070722f1 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 17:10:46 -1000 Subject: [PATCH 119/231] chore(.iex.exs): add IEx configurations --- .iex.exs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.iex.exs b/.iex.exs index 3e7c643e..d8518038 100644 --- a/.iex.exs +++ b/.iex.exs @@ -1,3 +1,6 @@ +IEx.configure(inspect: [charlists: :as_lists]) +IEx.configure(inspect: [limit: :infinity]) + alias EpochtalkServer.Repo alias EpochtalkServer.Models.Ban From 33041434c9700ba9e5b9dc75549eada37688cb23 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 17:11:28 -1000 Subject: [PATCH 120/231] refactor(user): formatUser -> format_user --- lib/epochtalk_server/models/user.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server/models/user.ex b/lib/epochtalk_server/models/user.ex index 9ff4748b..adf5154e 100644 --- a/lib/epochtalk_server/models/user.ex +++ b/lib/epochtalk_server/models/user.ex @@ -117,7 +117,7 @@ defmodule EpochtalkServer.Models.User do hc = primary_role.highlight_color Map.put(user, :role_name, primary_role.name) |> Map.put(:role_highlight_color, (if hc, do: hc, else: "")) - |> formatUser + |> format_user end end def by_username_and_password(username, password) @@ -166,7 +166,7 @@ defmodule EpochtalkServer.Models.User do changeset end end - defp formatUser(user) do + defp format_user(user) do user = Map.filter(user, fn {_, v} -> v end) # remove nil user = if f = Map.get(user, :fields), do: Map.merge(user, f), else: user # merge fields onto user user = if cc = Map.get(user, :collapsed_categories), do: Map.put(user, :collapsed_categories, Map.get(cc, "cats")), else: user # unnest cats From 8e4d295ddb755e67ff236dd586e186671ed04894 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 17:26:01 -1000 Subject: [PATCH 121/231] feat(board-moderator): get boards, Repo.all, map to board_ids --- lib/epochtalk_server/models/board_moderator.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/epochtalk_server/models/board_moderator.ex b/lib/epochtalk_server/models/board_moderator.ex index feb01e3f..186571c0 100644 --- a/lib/epochtalk_server/models/board_moderator.ex +++ b/lib/epochtalk_server/models/board_moderator.ex @@ -19,6 +19,7 @@ defmodule EpochtalkServer.Models.BoardModerator do end def get_boards(user_id) when is_integer(user_id) do - Repo.get_by(BoardModerator, user_id: user_id) + Repo.all(BoardModerator, user_id: user_id) + |> Enum.map(&(&1.board_id)) end end From a09bbced1c4319bc6f57bb55bcf871ce12b97c55 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 17:38:55 -1000 Subject: [PATCH 122/231] refactor(user): valid password, don't use model now called from auth_controller, split off from user by username --- lib/epochtalk_server/models/user.ex | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/epochtalk_server/models/user.ex b/lib/epochtalk_server/models/user.ex index adf5154e..ffe359ed 100644 --- a/lib/epochtalk_server/models/user.ex +++ b/lib/epochtalk_server/models/user.ex @@ -120,12 +120,7 @@ defmodule EpochtalkServer.Models.User do |> format_user end end - def by_username_and_password(username, password) - when is_binary(username) and is_binary(password) do - user = Repo.get_by(User, username: username) - if User.valid_password?(user, password), do: user - end - def valid_password?(%User{passhash: hashed_password}, password) + def valid_password?(%{passhash: hashed_password} = _user, password) when is_binary(hashed_password) and byte_size(password) > 0 do Argon2.verify_pass(password, hashed_password) end From 0bba24b1bd756958d58b34937c291d114f46c02b Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 17:39:55 -1000 Subject: [PATCH 123/231] refactor(auth-controller): one-line user query --- lib/epochtalk_server_web/controllers/auth_controller.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index c4a11f47..cbe3870a 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -66,8 +66,7 @@ defmodule EpochtalkServerWeb.AuthController do login(conn, Map.put(user_params, "rememberMe", false)) end def login(conn, %{"username" => username, "password" => password} = user_params) do - user = username - |> User.by_username + user = User.by_username(username) # check that user exists if !user, do: raise(InvalidCredentials) From 3925247b61bf9ab3bb9ec3248cd79616bcbce75e Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 17:41:32 -1000 Subject: [PATCH 124/231] feat(auth-controller): get moderated boards for user --- lib/epochtalk_server_web/controllers/auth_controller.ex | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index cbe3870a..ee2d4fff 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -2,6 +2,7 @@ defmodule EpochtalkServerWeb.AuthController do use EpochtalkServerWeb, :controller alias EpochtalkServer.Models.User alias EpochtalkServer.Models.Ban + alias EpochtalkServer.Models.BoardModerator alias EpochtalkServer.Repo alias EpochtalkServer.Auth.Guardian alias EpochtalkServer.Session @@ -84,6 +85,9 @@ defmodule EpochtalkServerWeb.AuthController do # unban if ban is expired and update roles user = Map.put(user, :roles, Ban.unban_if_expired(user)) + # get user's moderated boards + user = Map.put(user, :moderating, BoardModerator.get_boards(user.id)) + if user = User.by_username(username) do # TODO: check ban expiration # TODO: get moderated boards From b6ab6d5578b81f4f50f9d13d43ad75b6c3a3542f Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 17:41:51 -1000 Subject: [PATCH 125/231] feat(auth-controller): replace login_user call --- lib/epochtalk_server_web/controllers/auth_controller.ex | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index ee2d4fff..28acbaa8 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -88,13 +88,8 @@ defmodule EpochtalkServerWeb.AuthController do # get user's moderated boards user = Map.put(user, :moderating, BoardModerator.get_boards(user.id)) - if user = User.by_username(username) do - # TODO: check ban expiration - # TODO: get moderated boards - log_in_user(conn, user, user_params) - else - raise(InvalidCredentials) - end + # log the user in + log_in_user(conn, user, user_params) end defp log_in_user(conn, user, %{"rememberMe" => remember_me}) do datetime = NaiveDateTime.utc_now From c919015de3c4837586b52f3cf37acc2f39bf0f0c Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 17:45:36 -1000 Subject: [PATCH 126/231] refactor(auth-controller): move log_in_user code to login --- .../controllers/auth_controller.ex | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index 28acbaa8..016d78dd 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -88,29 +88,31 @@ defmodule EpochtalkServerWeb.AuthController do # get user's moderated boards user = Map.put(user, :moderating, BoardModerator.get_boards(user.id)) - # log the user in - log_in_user(conn, user, user_params) - end - defp log_in_user(conn, user, %{"rememberMe" => remember_me}) do + # build decoded token datetime = NaiveDateTime.utc_now session_id = UUID.uuid1() decoded_token = %{ user_id: user.id, session_id: session_id, timestamp: datetime } - # token expiration based on remember_me - ttl = case remember_me do + # set token expiration based on remember_me + ttl = case user.remember_me do # set longer expiration "true" -> {4, :weeks} # set default expiration _ -> {1, :day} end - token = conn + # sign user in and get encoded token + encoded_token = conn |> Guardian.Plug.sign_in(decoded_token, %{}, ttl: ttl) |> Guardian.Plug.current_token - user = Map.put(user, :token, token) + # add token to user + user = Map.put(user, :token, encoded_token) + + # save session Session.save(user) + # reply with user data conn |> render("credentials.json", user: user) end From 6d2cc160577a0e6504ab947ecdc5498991106c54 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Mon, 19 Sep 2022 17:47:11 -1000 Subject: [PATCH 127/231] fix(auth-controller): remember_me -> "rememberMe" --- lib/epochtalk_server_web/controllers/auth_controller.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index 016d78dd..87a9b3c3 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -93,8 +93,8 @@ defmodule EpochtalkServerWeb.AuthController do session_id = UUID.uuid1() decoded_token = %{ user_id: user.id, session_id: session_id, timestamp: datetime } - # set token expiration based on remember_me - ttl = case user.remember_me do + # set token expiration based on rememberMe + ttl = case Map.get(user, "rememberMe") do # set longer expiration "true" -> {4, :weeks} # set default expiration From dd141b08e3aff77619a906e7d0cc2062c6cef33d Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 20 Sep 2022 12:12:38 -1000 Subject: [PATCH 128/231] fix(by_username): handle when highlight color of primary role is nil --- lib/epochtalk_server/models/user.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epochtalk_server/models/user.ex b/lib/epochtalk_server/models/user.ex index ffe359ed..e518eae5 100644 --- a/lib/epochtalk_server/models/user.ex +++ b/lib/epochtalk_server/models/user.ex @@ -114,7 +114,7 @@ defmodule EpochtalkServer.Models.User do user = Map.put(user, :roles, Role.by_user_id(user.id)) # set primary role info primary_role = List.first(user[:roles]) - hc = primary_role.highlight_color + hc = Map.get(primary_role, :highlight_color) Map.put(user, :role_name, primary_role.name) |> Map.put(:role_highlight_color, (if hc, do: hc, else: "")) |> format_user From 26cd9f3c9ef27597f6f6961d0152e156f2637b7c Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 20 Sep 2022 12:13:15 -1000 Subject: [PATCH 129/231] feat(handle-ban-role): if user is banned, now only the banned role is returned --- lib/epochtalk_server/models/role.ex | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server/models/role.ex b/lib/epochtalk_server/models/role.ex index 60a420af..ac9009f1 100644 --- a/lib/epochtalk_server/models/role.ex +++ b/lib/epochtalk_server/models/role.ex @@ -48,9 +48,15 @@ defmodule EpochtalkServer.Models.Role do select: r, order_by: [asc: r.priority] case Repo.all(query) do - [] -> [get_default()] - result -> result + [] -> [get_default()] # user has no roles, return default role + users_roles -> users_roles # user has roles, return them end + |> handle_banned_user_role # if banned, only [ banned ] is returned for roles end defp get_default(), do: by_lookup("user") + + defp handle_banned_user_role(roles), do: if ban_role = reduce_ban_role(roles), do: [ban_role], else: roles + defp reduce_ban_role([]), do: nil + defp reduce_ban_role([role | _]) when role.lookup === "banned", do: role + defp reduce_ban_role([_ | roles]), do: reduce_ban_role(roles) end From 7ef9f79a1eed0287735e71b80d3814039381b1f6 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 20 Sep 2022 20:02:14 -1000 Subject: [PATCH 130/231] feat(role): implement get_masked_permissions to xor all user's role's permissions --- lib/epochtalk_server/models/role.ex | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/lib/epochtalk_server/models/role.ex b/lib/epochtalk_server/models/role.ex index ac9009f1..60979336 100644 --- a/lib/epochtalk_server/models/role.ex +++ b/lib/epochtalk_server/models/role.ex @@ -59,4 +59,34 @@ defmodule EpochtalkServer.Models.Role do defp reduce_ban_role([]), do: nil defp reduce_ban_role([role | _]) when role.lookup === "banned", do: role defp reduce_ban_role([_ | roles]), do: reduce_ban_role(roles) + + def get_masked_permissions(roles) when is_list(roles), do: Enum.reduce(roles, %{}, &get_masked_permissions(&2, &1)) + def get_masked_permissions(target, source) do + merge_keys = [:highlight_color, :permissions, :priority, :priority_restrictions] + filtered_source_keys = Map.keys(source) |> Enum.filter(&Enum.member?(merge_keys, &1)) + target_is_lesser_role = !Map.get(target, :priority) or target.priority > source.priority + Enum.reduce(filtered_source_keys, %{}, fn key, acc -> + case key do + :priority_restrictions -> # merge priority restrictions + source_pr = source.priority_restrictions + if target_is_lesser_role and !!source_pr and length(source_pr), + do: Map.put(acc, :priority_restrictions, source_pr), + else: Map.put(acc, :priority_restrictions, Map.get(target, :priority_restrictions)) + :permissions -> # merge permissions + target_permissions = if p = Map.get(target, key), do: p, else: %{} + Map.put(acc, key, deep_merge(target_permissions, Map.get(source, key))) + key when key in [:priority, :highlight_color] -> # merge priority/highlight_color + if target_is_lesser_role, do: Map.put(acc, key, Map.get(source, key)), else: Map.put(acc, key, Map.get(target, key)) + end + end) + end + + def deep_merge(left, right), do: Map.merge(left, right, &deep_resolve/3) + # Key exists in both maps, and both values are maps as well. + # These can be merged recursively. + defp deep_resolve(_key, left = %{}, right = %{}), do: deep_merge(left, right) + # Key exists in both maps, but at least one of the values is + # NOT a map. We fall back to standard merge behavior, preferring + # the value on the right. + defp deep_resolve(_key, _left, right), do: right end From 1fbb821be0bfcca884eff4f71253e957a9d98e6a Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 20 Sep 2022 20:03:30 -1000 Subject: [PATCH 131/231] fix(ban): unban_if_expire needs else case to return user's roles --- lib/epochtalk_server/models/ban.ex | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server/models/ban.ex b/lib/epochtalk_server/models/ban.ex index c386c002..f5368621 100644 --- a/lib/epochtalk_server/models/ban.ex +++ b/lib/epochtalk_server/models/ban.ex @@ -74,8 +74,10 @@ defmodule EpochtalkServer.Models.Ban do end # unban if ban is expired, return user's roles - def unban_if_expired(%{ban_expiration: expiration, id: user_id} = _user) do - if NaiveDateTime.compare(expiration, NaiveDateTime.utc_now) == :lt, do: unban(user_id) |> Map.get(:roles) + def unban_if_expired(%{ban_expiration: expiration, id: user_id} = user) do + if NaiveDateTime.compare(expiration, NaiveDateTime.utc_now) == :lt, + do: unban(user_id) |> Map.get(:roles), + else: user.roles end def unban_if_expired(user), do: user.roles end From 5238aa5551c2e16c2869d74f055b99215c45d678 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 20 Sep 2022 20:04:51 -1000 Subject: [PATCH 132/231] feat(login): functioning login, still need to handle session id, implemented format_user_reply --- lib/epochtalk_server_web/views/auth_view.ex | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/epochtalk_server_web/views/auth_view.ex b/lib/epochtalk_server_web/views/auth_view.ex index a8ccaa42..ab26e3f9 100644 --- a/lib/epochtalk_server_web/views/auth_view.ex +++ b/lib/epochtalk_server_web/views/auth_view.ex @@ -1,5 +1,6 @@ defmodule EpochtalkServerWeb.AuthView do use EpochtalkServerWeb, :view + alias EpochtalkServer.Models.Role def render("search.json", %{found: found}) do %{found: found} @@ -9,7 +10,7 @@ defmodule EpochtalkServerWeb.AuthView do user |> user_json() end def render("credentials.json", %{user: user}) do - user |> credentials_json() + user |> format_user_reply() end def user_json(user) do %{ @@ -21,18 +22,22 @@ defmodule EpochtalkServerWeb.AuthView do roles: %{} # user.roles } end - defp credentials_json(user) do - %{ + defp format_user_reply(user) do + reply = %{ token: user.token, id: user.id, username: user.username, - # TODO: fill in these fields avatar: Map.get(user, :avatar), # user.avatar - permissions: %{}, # user.permissions + permissions: Role.get_masked_permissions(user.roles), # user.permissions moderating: Map.get(user, :moderating), # user.moderating - roles: user.roles, - ban_expiration: Map.get(user, :ban_expiration), - malicious_score: Map.get(user, :malicious_score) + roles: Enum.map(user.roles, &(&1.lookup)) } + # only append ban_expiration and malicious_score if present + if exp = Map.get(user, :ban_expiration), + do: Map.put(reply, :ban_expiration, exp), + else: reply + if ms = Map.get(user, :malicious_score), + do: Map.put(reply, :malicious_score, ms), + else: reply end end From 67e05cea3961c12700c3e07af784872717bac93c Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 20 Sep 2022 20:08:09 -1000 Subject: [PATCH 133/231] fix(format_user_reply): issue with ban_expiration and malicious_score not being appended to reply --- lib/epochtalk_server_web/views/auth_view.ex | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server_web/views/auth_view.ex b/lib/epochtalk_server_web/views/auth_view.ex index ab26e3f9..82075146 100644 --- a/lib/epochtalk_server_web/views/auth_view.ex +++ b/lib/epochtalk_server_web/views/auth_view.ex @@ -33,11 +33,12 @@ defmodule EpochtalkServerWeb.AuthView do roles: Enum.map(user.roles, &(&1.lookup)) } # only append ban_expiration and malicious_score if present - if exp = Map.get(user, :ban_expiration), + reply = if exp = Map.get(user, :ban_expiration), do: Map.put(reply, :ban_expiration, exp), else: reply - if ms = Map.get(user, :malicious_score), + reply = if ms = Map.get(user, :malicious_score), do: Map.put(reply, :malicious_score, ms), else: reply + reply end end From 6066aa2033eebe9938d6a92bf83151c8d4ee0ffe Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 20 Sep 2022 20:13:46 -1000 Subject: [PATCH 134/231] fix(board-moderator): issue with get_boards always returning all records --- lib/epochtalk_server/models/board_moderator.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/epochtalk_server/models/board_moderator.ex b/lib/epochtalk_server/models/board_moderator.ex index 186571c0..9fc8e24b 100644 --- a/lib/epochtalk_server/models/board_moderator.ex +++ b/lib/epochtalk_server/models/board_moderator.ex @@ -1,6 +1,7 @@ defmodule EpochtalkServer.Models.BoardModerator do use Ecto.Schema import Ecto.Changeset + import Ecto.Query, only: [from: 2] alias EpochtalkServer.Repo alias EpochtalkServer.Models.User alias EpochtalkServer.Models.Board @@ -19,7 +20,8 @@ defmodule EpochtalkServer.Models.BoardModerator do end def get_boards(user_id) when is_integer(user_id) do - Repo.all(BoardModerator, user_id: user_id) + from(bm in BoardModerator, where: bm.user_id == ^user_id) + |> Repo.all() |> Enum.map(&(&1.board_id)) end end From 77f971872ea12908bb85c45dfa52cb02ca64b5dc Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 22 Sep 2022 11:33:21 -1000 Subject: [PATCH 135/231] refactor(role): privatize methods get_masked_permissions deep_merge --- lib/epochtalk_server/models/role.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server/models/role.ex b/lib/epochtalk_server/models/role.ex index 60979336..df7725ef 100644 --- a/lib/epochtalk_server/models/role.ex +++ b/lib/epochtalk_server/models/role.ex @@ -61,7 +61,7 @@ defmodule EpochtalkServer.Models.Role do defp reduce_ban_role([_ | roles]), do: reduce_ban_role(roles) def get_masked_permissions(roles) when is_list(roles), do: Enum.reduce(roles, %{}, &get_masked_permissions(&2, &1)) - def get_masked_permissions(target, source) do + defp get_masked_permissions(target, source) do merge_keys = [:highlight_color, :permissions, :priority, :priority_restrictions] filtered_source_keys = Map.keys(source) |> Enum.filter(&Enum.member?(merge_keys, &1)) target_is_lesser_role = !Map.get(target, :priority) or target.priority > source.priority @@ -81,7 +81,7 @@ defmodule EpochtalkServer.Models.Role do end) end - def deep_merge(left, right), do: Map.merge(left, right, &deep_resolve/3) + defp deep_merge(left, right), do: Map.merge(left, right, &deep_resolve/3) # Key exists in both maps, and both values are maps as well. # These can be merged recursively. defp deep_resolve(_key, left = %{}, right = %{}), do: deep_merge(left, right) From ea4cfa445816579b8d6e91c2347b928646cc3531 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 22 Sep 2022 13:19:44 -1000 Subject: [PATCH 136/231] refactor(auth/session): move token creation to session.login from auth --- lib/epochtalk_server/session.ex | 33 +++++++++++++++++++ .../controllers/auth_controller.ex | 25 ++------------ 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index b027b38b..fffdff28 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -1,4 +1,37 @@ defmodule EpochtalkServer.Session do + alias Guardian + + # set user session id, timestamp, ttl + # log user in with Guardian to get token + # save user session info to redis + # return user with token + def login(user, conn) do + datetime = NaiveDateTime.utc_now + session_id = UUID.uuid1() + decoded_token = %{ user_id: user.id, session_id: session_id, timestamp: datetime } + + # set token expiration based on rememberMe + ttl = case Map.get(user, "rememberMe") do + # set longer expiration + "true" -> {4, :weeks} + # set default expiration + _ -> {1, :day} + end + + # sign user in and get encoded token + encoded_token = conn + |> Guardian.Plug.sign_in(decoded_token, %{}, ttl: ttl) + |> Guardian.Plug.current_token + + # add token to user + user = Map.put(user, :token, encoded_token) + + # save session + save(user, session_id) + + # return user with token + user + end def save(db_user) do # TODO: return role lookups from db instead of entire roles update_user_info(db_user.id, db_user.username, db_user.avatar) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index 87a9b3c3..aee446bc 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -88,29 +88,8 @@ defmodule EpochtalkServerWeb.AuthController do # get user's moderated boards user = Map.put(user, :moderating, BoardModerator.get_boards(user.id)) - # build decoded token - datetime = NaiveDateTime.utc_now - session_id = UUID.uuid1() - decoded_token = %{ user_id: user.id, session_id: session_id, timestamp: datetime } - - # set token expiration based on rememberMe - ttl = case Map.get(user, "rememberMe") do - # set longer expiration - "true" -> {4, :weeks} - # set default expiration - _ -> {1, :day} - end - - # sign user in and get encoded token - encoded_token = conn - |> Guardian.Plug.sign_in(decoded_token, %{}, ttl: ttl) - |> Guardian.Plug.current_token - - # add token to user - user = Map.put(user, :token, encoded_token) - - # save session - Session.save(user) + # create session + Session.login(user, conn) # reply with user data conn From f5c4ac8d96fdeb959080414d8cedbb5a15efc90f Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 22 Sep 2022 13:20:52 -1000 Subject: [PATCH 137/231] refactor(auth/session): Session.login -> create --- lib/epochtalk_server/session.ex | 2 +- lib/epochtalk_server_web/controllers/auth_controller.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index fffdff28..f034dd5d 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -5,7 +5,7 @@ defmodule EpochtalkServer.Session do # log user in with Guardian to get token # save user session info to redis # return user with token - def login(user, conn) do + def create(user, conn) do datetime = NaiveDateTime.utc_now session_id = UUID.uuid1() decoded_token = %{ user_id: user.id, session_id: session_id, timestamp: datetime } diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index aee446bc..61bdba47 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -89,7 +89,7 @@ defmodule EpochtalkServerWeb.AuthController do user = Map.put(user, :moderating, BoardModerator.get_boards(user.id)) # create session - Session.login(user, conn) + Session.create(user, conn) # reply with user data conn From 51b4b4d9a191b6b8c59bb81cf2685006534e8e6c Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 22 Sep 2022 13:21:48 -1000 Subject: [PATCH 138/231] feat(session): implement session set/delete/get by session_id, user_id --- lib/epochtalk_server/session.ex | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index f034dd5d..a92ff8dc 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -88,6 +88,28 @@ defmodule EpochtalkServer.Session do Redix.command(:redix, ["HDEL", ban_key, "ban_expiration", "malicious_score"]) Redix.command(:redix, ["HSET", ban_key, ban_info]) end + defp set_session(user_id, session_id) do + # save session id to redis under "user:{user_id}:sessions" + session_key = generate_key(user_id, "sessions") + Redix.command(:redix, ["SADD", session_key, session_id]) + end + defp get_sessions(user_id) do + # get session id's from redis under "user:{user_id}:sessions" + session_key = generate_key(user_id, "sessions") + Redix.command(:redix, ["SMEMBERS", session_key]) + end + defp delete_session(user_id, session_id) do + # delete session id from redis under "user:{user_id}:sessions" + session_key = generate_key(user_id, "sessions") + Redix.command(:redix, ["SREM", session_key, session_id]) + end + defp delete_sessions(user_id, session_id) do + # delete session id from redis under "user:{user_id}:sessions" + session_key = generate_key(user_id, "sessions") + Redix.command(:redix, ["SPOP", session_key, session_id]) + # repeat until redix returns nil + |> unless do delete_sessions(user_id, session_id) end + end defp generate_key(user_id, "user"), do: "user:#{user_id}" defp generate_key(user_id, type), do: "user:#{user_id}:#{type}" end From 29aca0f631668e43432160c830100918e226c9bb Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 22 Sep 2022 13:22:39 -1000 Subject: [PATCH 139/231] refactor(session.save): privatize and add arg session_id --- lib/epochtalk_server/session.ex | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index a92ff8dc..ef57d7a1 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -32,7 +32,7 @@ defmodule EpochtalkServer.Session do # return user with token user end - def save(db_user) do + defp save(db_user, session_id) do # TODO: return role lookups from db instead of entire roles update_user_info(db_user.id, db_user.username, db_user.avatar) update_roles(db_user.id, db_user.roles) @@ -40,14 +40,7 @@ defmodule EpochtalkServer.Session do ban_info = if Map.has_key?(db_user, :malicious_score), do: Map.put(ban_info, :malicious_score, db_user.malicious_score) update_ban_info(db_user.id, ban_info) update_moderating(db_user.id, Map.get(db_user, :moderating)) - # TODO: do this outside of here, need guardian token - # -- or don't do it if we don't need this functionality - # // save user-session to redis set under "user:{user_id}:sessions" - # .then(function() { - # var userSessionKey = 'user:' + dbUser.id + ':sessions'; - # return redis.saddAsync(userSessionKey, decodedToken.sessionId); - # }) - # .then(function() { return formatUserReply(token, dbUser); }); + set_session(db_user.id, session_id) end # use default role def update_roles(user_id, roles) when is_list(roles) do From b76f00462ad51b1a43b53bc89b7a9ade4a1fcdbe Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 22 Sep 2022 13:26:06 -1000 Subject: [PATCH 140/231] docs(session): TODO invalidate token when deleting session --- lib/epochtalk_server/session.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index ef57d7a1..b3580a94 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -91,6 +91,7 @@ defmodule EpochtalkServer.Session do session_key = generate_key(user_id, "sessions") Redix.command(:redix, ["SMEMBERS", session_key]) end + # TODO: invalidate token defp delete_session(user_id, session_id) do # delete session id from redis under "user:{user_id}:sessions" session_key = generate_key(user_id, "sessions") From c45fe52681e3c92f3d325b7c58bddaf0d89dd7af Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 22 Sep 2022 15:50:37 -1000 Subject: [PATCH 141/231] feat(roles): implement by_lookup for list of lookups --- lib/epochtalk_server/models/role.ex | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/epochtalk_server/models/role.ex b/lib/epochtalk_server/models/role.ex index df7725ef..a04a5709 100644 --- a/lib/epochtalk_server/models/role.ex +++ b/lib/epochtalk_server/models/role.ex @@ -27,6 +27,9 @@ defmodule EpochtalkServer.Models.Role do |> validate_required([:name, :description, :lookup, :priority, :permissions]) end def all, do: from(r in Role, order_by: r.id) |> Repo.all + def by_lookup(lookups) when is_list(lookups) do + from(r in Role, where: r.lookup in ^lookups) |> Repo.all + end def by_lookup(lookup), do: Repo.get_by(Role, lookup: lookup) def insert([]), do: {:error, "Role list is empty"} def insert(%Role{} = role), do: Repo.insert(role) From 6e6afd3912abb0832cfc5970e82ba8eb3f058dc8 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 22 Sep 2022 14:35:02 -1000 Subject: [PATCH 142/231] fix(session): correct Guardian alias --- lib/epochtalk_server/session.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index b3580a94..98d74e2e 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -1,5 +1,5 @@ defmodule EpochtalkServer.Session do - alias Guardian + alias EpochtalkServer.Auth.Guardian # set user session id, timestamp, ttl # log user in with Guardian to get token From 8a924f6969980fab10119724de884f8a83f5aed0 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 22 Sep 2022 14:35:50 -1000 Subject: [PATCH 143/231] refactor(auth): underscore unused variable --- lib/epochtalk_server_web/controllers/auth_controller.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index 61bdba47..88541751 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -66,7 +66,7 @@ defmodule EpochtalkServerWeb.AuthController do def login(conn, user_params) when not is_map_key(user_params, "rememberMe") do login(conn, Map.put(user_params, "rememberMe", false)) end - def login(conn, %{"username" => username, "password" => password} = user_params) do + def login(conn, %{"username" => username, "password" => password} = _user_params) do user = User.by_username(username) # check that user exists From e40877f849d8219c85a577399f530482a57a3b77 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 22 Sep 2022 14:39:16 -1000 Subject: [PATCH 144/231] fix(session.create): modify and return conn/user --- lib/epochtalk_server/session.ex | 7 +++---- lib/epochtalk_server_web/controllers/auth_controller.ex | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index 98d74e2e..9249e078 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -19,9 +19,8 @@ defmodule EpochtalkServer.Session do end # sign user in and get encoded token - encoded_token = conn - |> Guardian.Plug.sign_in(decoded_token, %{}, ttl: ttl) - |> Guardian.Plug.current_token + conn = Guardian.Plug.sign_in(conn, decoded_token, %{}, ttl: ttl) + encoded_token = Guardian.Plug.current_token(conn) # add token to user user = Map.put(user, :token, encoded_token) @@ -30,7 +29,7 @@ defmodule EpochtalkServer.Session do save(user, session_id) # return user with token - user + {user, conn} end defp save(db_user, session_id) do # TODO: return role lookups from db instead of entire roles diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index 88541751..1b0f8be2 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -89,7 +89,7 @@ defmodule EpochtalkServerWeb.AuthController do user = Map.put(user, :moderating, BoardModerator.get_boards(user.id)) # create session - Session.create(user, conn) + {user, conn} = Session.create(user, conn) # reply with user data conn From 04767d7511b227f23423ceef6fa11ed4beb835ad Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 22 Sep 2022 15:48:37 -1000 Subject: [PATCH 145/231] feat(guardian/session): handle session_id in resource from claims store session_id in subject for token extract user_id and session_id in resource from claims check that session is active in redis before returning resource set session_id in resource --- lib/epochtalk_server/auth/guardian.ex | 41 ++++++++++++++++++--------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/lib/epochtalk_server/auth/guardian.ex b/lib/epochtalk_server/auth/guardian.ex index 93342e7d..f5b144b6 100644 --- a/lib/epochtalk_server/auth/guardian.ex +++ b/lib/epochtalk_server/auth/guardian.ex @@ -2,34 +2,47 @@ defmodule EpochtalkServer.Auth.Guardian do use Guardian, otp_app: :epochtalk_server alias EpochtalkServer.Models.User - def subject_for_token(%{user_id: user_id}, _claims) do + def subject_for_token(%{user_id: user_id, session_id: session_id}, _claims) do # You can use any value for the subject of your token but # it should be useful in retrieving the resource later, see # how it being used on `resource_from_claims/1` function. # A unique `id` is a good subject, a non-unique email address # is a poor subject. - sub = to_string(user_id) + sub = to_string(user_id) <> ":" <> session_id {:ok, sub} end def subject_for_token(_, _) do {:error, :reason_for_error} end - def resource_from_claims(%{"sub" => user_id}) do + def resource_from_claims(%{"sub" => sub}) do # Here we'll look up our resource from the claims, the subject can be # found in the `"sub"` key. In above `subject_for_token/2` we returned # the resource id so here we'll rely on that to look it up. - resource = %{ - id: user_id, - # session_id: ?, - username: Redix.command!(:redix, ["HGET", "user:#{user_id}", "username"]), - avatar: Redix.command!(:redix, ["HGET", "user:#{user_id}", "avatar"]), - roles: Redix.command!(:redix, ["SMEMBERS", "user:#{user_id}:roles"]), - moderating: Redix.command!(:redix, ["GET", "user:#{user_id}:moderating"]), - ban_expiration: Redix.command!(:redix, ["HEXISTS", "user:#{user_id}:ban_info", "ban_expiration"]), - malicious_score: Redix.command!(:redix, ["HEXISTS", "user:#{user_id}:ban_info", "malicious_score"]) - } - {:ok, resource} + + # extract user_id, session_id from subject + [user_id, session_id] = String.split(sub, ":") + + # check if session is active in redis + Redix.command!(:redix, ["SISMEMBER", "user:#{user_id}:sessions", session_id]) + |> case do + 0 -> + # session is not active, return error + {:error, "No session with id #{session_id}"} + 1 -> + # session is active, populate data + resource = %{ + id: user_id, + session_id: session_id, + username: Redix.command!(:redix, ["HGET", "user:#{user_id}", "username"]), + avatar: Redix.command!(:redix, ["HGET", "user:#{user_id}", "avatar"]), + roles: Redix.command!(:redix, ["SMEMBERS", "user:#{user_id}:roles"]), + moderating: Redix.command!(:redix, ["SMEMBERS", "user:#{user_id}:moderating"]), + ban_expiration: Redix.command!(:redix, ["HEXISTS", "user:#{user_id}:ban_info", "ban_expiration"]), + malicious_score: Redix.command!(:redix, ["HEXISTS", "user:#{user_id}:ban_info", "malicious_score"]) + } + {:ok, resource} + end end def resource_from_claims(_claims) do {:error, :reason_for_error} From 646a2d754d0006837b68d5b8799615ca0e1eb716 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 22 Sep 2022 15:50:51 -1000 Subject: [PATCH 146/231] feat(auth-controller): verify authentication on login if user is already logged in, return authenticate() otherwise, attempt to log user in --- .../controllers/auth_controller.ex | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index 1b0f8be2..62a40ca5 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -41,6 +41,7 @@ defmodule EpochtalkServerWeb.AuthController do |> render("400.json", %{message: inspect(changeset.errors)}) end end + def authenticate(conn), do: authenticate(conn, %{}) def authenticate(conn, _attrs) do user = conn |> Guardian.Plug.current_resource @@ -67,32 +68,36 @@ defmodule EpochtalkServerWeb.AuthController do login(conn, Map.put(user_params, "rememberMe", false)) end def login(conn, %{"username" => username, "password" => password} = _user_params) do - user = User.by_username(username) + if Guardian.Plug.authenticated?(conn) do + authenticate(conn) + else + user = User.by_username(username) - # check that user exists - if !user, do: raise(InvalidCredentials) + # check that user exists + if !user, do: raise(InvalidCredentials) - # check confirmation token - if Map.get(user, :confirmation_token), do: raise(AccountNotConfirmed) + # check confirmation token + if Map.get(user, :confirmation_token), do: raise(AccountNotConfirmed) - # check user migration - # if passhash doesn't exist, account hasn't been fully migrated - if !Map.get(user, :passhash), do: raise(AccountMigrationNotComplete) + # check user migration + # if passhash doesn't exist, account hasn't been fully migrated + if !Map.get(user, :passhash), do: raise(AccountMigrationNotComplete) - # check password - if !User.valid_password?(user, password), do: raise(InvalidCredentials) + # check password + if !User.valid_password?(user, password), do: raise(InvalidCredentials) - # unban if ban is expired and update roles - user = Map.put(user, :roles, Ban.unban_if_expired(user)) + # unban if ban is expired and update roles + user = Map.put(user, :roles, Ban.unban_if_expired(user)) - # get user's moderated boards - user = Map.put(user, :moderating, BoardModerator.get_boards(user.id)) + # get user's moderated boards + user = Map.put(user, :moderating, BoardModerator.get_boards(user.id)) - # create session - {user, conn} = Session.create(user, conn) + # create session + {user, conn} = Session.create(user, conn) - # reply with user data - conn - |> render("credentials.json", user: user) + # reply with user data + conn + |> render("credentials.json", user: user) + end end end From 1abb0fcba72ce9d92634b2cc446d400f5b580611 Mon Sep 17 00:00:00 2001 From: unenglishable Date: Thu, 22 Sep 2022 15:55:05 -1000 Subject: [PATCH 147/231] feat(guardian-resource-from-claims): get roles by lookup will be passed to format_user_reply, which expects this format --- lib/epochtalk_server/auth/guardian.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/epochtalk_server/auth/guardian.ex b/lib/epochtalk_server/auth/guardian.ex index f5b144b6..08375531 100644 --- a/lib/epochtalk_server/auth/guardian.ex +++ b/lib/epochtalk_server/auth/guardian.ex @@ -1,6 +1,7 @@ defmodule EpochtalkServer.Auth.Guardian do use Guardian, otp_app: :epochtalk_server alias EpochtalkServer.Models.User + alias EpochtalkServer.Models.Role def subject_for_token(%{user_id: user_id, session_id: session_id}, _claims) do # You can use any value for the subject of your token but @@ -36,7 +37,7 @@ defmodule EpochtalkServer.Auth.Guardian do session_id: session_id, username: Redix.command!(:redix, ["HGET", "user:#{user_id}", "username"]), avatar: Redix.command!(:redix, ["HGET", "user:#{user_id}", "avatar"]), - roles: Redix.command!(:redix, ["SMEMBERS", "user:#{user_id}:roles"]), + roles: Redix.command!(:redix, ["SMEMBERS", "user:#{user_id}:roles"]) |> Role.by_lookup, moderating: Redix.command!(:redix, ["SMEMBERS", "user:#{user_id}:moderating"]), ban_expiration: Redix.command!(:redix, ["HEXISTS", "user:#{user_id}:ban_info", "ban_expiration"]), malicious_score: Redix.command!(:redix, ["HEXISTS", "user:#{user_id}:ban_info", "malicious_score"]) From 737fc7248ebb9e08c4aaf2838349c332a4aae497 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Fri, 23 Sep 2022 11:46:11 -1000 Subject: [PATCH 148/231] fix(errors): convert raised errors into expect http error json response --- lib/epochtalk_server_web/views/custom_errors.ex | 9 ++++----- lib/epochtalk_server_web/views/error_view.ex | 8 ++++++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/epochtalk_server_web/views/custom_errors.ex b/lib/epochtalk_server_web/views/custom_errors.ex index b6c5f801..fce99e55 100644 --- a/lib/epochtalk_server_web/views/custom_errors.ex +++ b/lib/epochtalk_server_web/views/custom_errors.ex @@ -1,29 +1,28 @@ defmodule EpochtalkServerWeb.CustomErrors do - auth_prefix = "Auth:" # Auth errors defmodule InvalidCredentials do @moduledoc """ Exception raised when user and password are not correct """ - defexception plug_status: 400, message: "#{auth_prefix} Invalid credentials", conn: nil, router: nil + defexception plug_status: 400, message: "Invalid credentials", conn: nil, router: nil end defmodule NotLoggedIn do @moduledoc """ Exception raised when user is not logged in """ - defexception plug_status: 400, message: "#{auth_prefix} Not logged in", conn: nil, router: nil + defexception plug_status: 400, message: "Not logged in", conn: nil, router: nil end defmodule AccountNotConfirmed do @moduledoc """ Exception raised when user's account is not confirmed """ - defexception plug_status: 400, message: "#{auth_prefix} User account not confirmed", conn: nil, router: nil + defexception plug_status: 400, message: "User account not confirmed", conn: nil, router: nil end defmodule AccountMigrationNotComplete do @moduledoc """ Exception raised when user's account is not fully migrated """ - defexception plug_status: 403, message: "#{auth_prefix} User account migration not complete, please reset password", conn: nil, router: nil + defexception plug_status: 403, message: "User account migration not complete, please reset password", conn: nil, router: nil end end diff --git a/lib/epochtalk_server_web/views/error_view.ex b/lib/epochtalk_server_web/views/error_view.ex index 449d69dc..9222d66f 100644 --- a/lib/epochtalk_server_web/views/error_view.ex +++ b/lib/epochtalk_server_web/views/error_view.ex @@ -4,7 +4,11 @@ defmodule EpochtalkServerWeb.ErrorView do # By default, Phoenix returns the status message from # the template name. For example, "404.json" becomes # "Not Found". - def template_not_found(template, _assigns) do - %{message: Phoenix.Controller.status_message_from_template(template)} + def template_not_found(template, assigns) do + %{ + status: assigns.status, + message: assigns.reason.message, + error: Phoenix.Controller.status_message_from_template(template) + } end end From ea6aa80246dbafe755f164418aac110002621ad6 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Fri, 23 Sep 2022 14:29:34 -1000 Subject: [PATCH 149/231] feat(guardian-errors): handle guardian errors using error view --- lib/epochtalk_server_web/views/error_view.ex | 9 +++++++++ .../views/guardian_error_handler.ex | 15 +++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/lib/epochtalk_server_web/views/error_view.ex b/lib/epochtalk_server_web/views/error_view.ex index 9222d66f..1b9c3deb 100644 --- a/lib/epochtalk_server_web/views/error_view.ex +++ b/lib/epochtalk_server_web/views/error_view.ex @@ -11,4 +11,13 @@ defmodule EpochtalkServerWeb.ErrorView do error: Phoenix.Controller.status_message_from_template(template) } end + + # Handles auth errors coming from guardian + def render("Guardian401.json", assigns) do + %{ + status: assigns.conn.status, + message: assigns.message, + error: Phoenix.Controller.status_message_from_template(to_string(assigns.conn.status)) + } + end end diff --git a/lib/epochtalk_server_web/views/guardian_error_handler.ex b/lib/epochtalk_server_web/views/guardian_error_handler.ex index fd4fd8a8..40ee8ead 100644 --- a/lib/epochtalk_server_web/views/guardian_error_handler.ex +++ b/lib/epochtalk_server_web/views/guardian_error_handler.ex @@ -1,13 +1,16 @@ defmodule EpochtalkServerWeb.GuardianErrorHandler do - import Plug.Conn + use Phoenix.Controller + alias EpochtalkServerWeb.ErrorView + # This is used to convert errors coming out of the guardian pipelines into a 401 + # renders error tuple from guardian pipeline into a json error response @behaviour Guardian.Plug.ErrorHandler - @impl Guardian.Plug.ErrorHandler - def auth_error(conn, {type, reason}, opts) do - # TODO: try using error view here + def auth_error(conn, {type, _reason}, _opts) do + message = to_string(type) |> String.replace("_", " ") |> String.capitalize conn - |> put_resp_content_type("text/plain") - |> send_resp(401, inspect({type, reason})) + |> put_status(401) + |> put_view(ErrorView) + |> render("Guardian401.json", message: message) end end From 9b1f782d7ff4b2d5b93587f65ad714f6289d3baf Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 26 Sep 2022 13:00:05 -1000 Subject: [PATCH 150/231] refactor(user-formatting): dont show moderating, ban_expiration or malicious score if not present --- lib/epochtalk_server/auth/guardian.ex | 21 ++++++++++----------- lib/epochtalk_server_web/views/auth_view.ex | 6 ++++-- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/lib/epochtalk_server/auth/guardian.ex b/lib/epochtalk_server/auth/guardian.ex index 08375531..a5d757f9 100644 --- a/lib/epochtalk_server/auth/guardian.ex +++ b/lib/epochtalk_server/auth/guardian.ex @@ -1,6 +1,5 @@ defmodule EpochtalkServer.Auth.Guardian do use Guardian, otp_app: :epochtalk_server - alias EpochtalkServer.Models.User alias EpochtalkServer.Models.Role def subject_for_token(%{user_id: user_id, session_id: session_id}, _claims) do @@ -12,9 +11,7 @@ defmodule EpochtalkServer.Auth.Guardian do sub = to_string(user_id) <> ":" <> session_id {:ok, sub} end - def subject_for_token(_, _) do - {:error, :reason_for_error} - end + def subject_for_token(_, _), do: {:error, :reason_for_error} def resource_from_claims(%{"sub" => sub}) do # Here we'll look up our resource from the claims, the subject can be @@ -37,17 +34,19 @@ defmodule EpochtalkServer.Auth.Guardian do session_id: session_id, username: Redix.command!(:redix, ["HGET", "user:#{user_id}", "username"]), avatar: Redix.command!(:redix, ["HGET", "user:#{user_id}", "avatar"]), - roles: Redix.command!(:redix, ["SMEMBERS", "user:#{user_id}:roles"]) |> Role.by_lookup, - moderating: Redix.command!(:redix, ["SMEMBERS", "user:#{user_id}:moderating"]), - ban_expiration: Redix.command!(:redix, ["HEXISTS", "user:#{user_id}:ban_info", "ban_expiration"]), - malicious_score: Redix.command!(:redix, ["HEXISTS", "user:#{user_id}:ban_info", "malicious_score"]) + roles: Redix.command!(:redix, ["SMEMBERS", "user:#{user_id}:roles"]) |> Role.by_lookup } + # only append moderating, ban_expiration and malicious_score if present + resource = if length(moderating = Redix.command!(:redix, ["SMEMBERS", "user:#{user_id}:moderating"])) != 0, + do: Map.put(resource, :moderating, moderating), else: resource + resource = if (ban_expiration = Redix.command!(:redix, ["HEXISTS", "user:#{user_id}:ban_info", "ban_expiration"])) != 0, + do: Map.put(resource, :ban_expiration, ban_expiration), else: resource + resource = if (malicious_score = Redix.command!(:redix, ["HEXISTS", "user:#{user_id}:ban_info", "malicious_score"])) != 0, + do: Map.put(resource, :malicious_score, malicious_score), else: resource {:ok, resource} end end - def resource_from_claims(_claims) do - {:error, :reason_for_error} - end + def resource_from_claims(_claims), do: {:error, :reason_for_error} def after_encode_and_sign(resource, claims, token, _options) do with {:ok, _} <- Guardian.DB.after_encode_and_sign(resource, claims["typ"], claims, token) do diff --git a/lib/epochtalk_server_web/views/auth_view.ex b/lib/epochtalk_server_web/views/auth_view.ex index 82075146..10421971 100644 --- a/lib/epochtalk_server_web/views/auth_view.ex +++ b/lib/epochtalk_server_web/views/auth_view.ex @@ -29,10 +29,12 @@ defmodule EpochtalkServerWeb.AuthView do username: user.username, avatar: Map.get(user, :avatar), # user.avatar permissions: Role.get_masked_permissions(user.roles), # user.permissions - moderating: Map.get(user, :moderating), # user.moderating roles: Enum.map(user.roles, &(&1.lookup)) } - # only append ban_expiration and malicious_score if present + # only append moderating, ban_expiration and malicious_score if present + reply = if length(moderating = Map.get(user, :moderating)) != 0, + do: Map.put(reply, :moderating, moderating), + else: reply reply = if exp = Map.get(user, :ban_expiration), do: Map.put(reply, :ban_expiration, exp), else: reply From aeb90b5ceed15111e6e7b9405ba4da7c2b03dead Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 26 Sep 2022 14:44:39 -1000 Subject: [PATCH 151/231] refactor(user-create): rename User.create_user to User.create --- lib/epochtalk_server/models/user.ex | 6 +++--- priv/repo/seed_user.exs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/epochtalk_server/models/user.ex b/lib/epochtalk_server/models/user.ex index e518eae5..9a4d0faa 100644 --- a/lib/epochtalk_server/models/user.ex +++ b/lib/epochtalk_server/models/user.ex @@ -38,14 +38,14 @@ defmodule EpochtalkServer.Models.User do |> validate_password() end # create admin, for seeding - def create_user(user_attrs, true = _admin) do + def create(user_attrs, true = _admin) do Repo.transaction(fn -> - create_user(user_attrs) + create(user_attrs) |> case do {:ok, %{ id: id } = _user} -> RoleUser.set_admin(id) end end) end # create user, for seeding - def create_user(user_attrs) do + def create(user_attrs) do %User{} |> User.registration_changeset(user_attrs) |> Repo.insert diff --git a/priv/repo/seed_user.exs b/priv/repo/seed_user.exs index 4701949d..c6307867 100644 --- a/priv/repo/seed_user.exs +++ b/priv/repo/seed_user.exs @@ -3,9 +3,9 @@ System.argv() |> case do [username, email, password, "admin" = _admin] -> admin = true - User.create_user(%{username: username, email: email, password: password}, admin) + User.create(%{username: username, email: email, password: password}, admin) [username, email, password] -> - User.create_user(%{username: username, email: email, password: password}) + User.create(%{username: username, email: email, password: password}) _ -> IO.puts("Usage: mix seed.user [admin]") {:error, "Invalid arguments"} From 5c296cfc2d8b976e6d5c8c8d6792ec29bba5a526 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 26 Sep 2022 14:45:28 -1000 Subject: [PATCH 152/231] fix(username-validation): set min max for username length --- lib/epochtalk_server/models/user.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/epochtalk_server/models/user.ex b/lib/epochtalk_server/models/user.ex index 9a4d0faa..dbed2930 100644 --- a/lib/epochtalk_server/models/user.ex +++ b/lib/epochtalk_server/models/user.ex @@ -127,6 +127,7 @@ defmodule EpochtalkServer.Models.User do defp validate_username(changeset) do changeset |> validate_required(:username) + |> validate_length(:username, min: 3, max: 255) |> unique_constraint(:username) end defp validate_email(changeset) do From 1d2d9791f9fe93bd606b0d20259490c9fcccbf19 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 27 Sep 2022 11:46:13 -1000 Subject: [PATCH 153/231] feat(ban): add ban/1 --- lib/epochtalk_server/models/ban.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/epochtalk_server/models/ban.ex b/lib/epochtalk_server/models/ban.ex index f5368621..fe8572e0 100644 --- a/lib/epochtalk_server/models/ban.ex +++ b/lib/epochtalk_server/models/ban.ex @@ -49,6 +49,7 @@ defmodule EpochtalkServer.Models.Ban do end def by_user_id(user_id) when is_integer(user_id), do: Repo.get_by(Ban, user_id: user_id) + def ban(user_id), do: ban(user_id, nil) def ban(user_id, expiration) do Repo.transaction(fn -> RoleUser.set_user_role(Role.get_banned_role_id, user_id) From 34f350ea080b45f3f395f8c8a0329752950d47fe Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 27 Sep 2022 11:46:39 -1000 Subject: [PATCH 154/231] fix(session-avatar): handle if avatar is nil going into session --- lib/epochtalk_server/session.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index 9249e078..018f85a8 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -33,7 +33,7 @@ defmodule EpochtalkServer.Session do end defp save(db_user, session_id) do # TODO: return role lookups from db instead of entire roles - update_user_info(db_user.id, db_user.username, db_user.avatar) + update_user_info(db_user.id, db_user.username, Map.get(db_user,:avatar)) update_roles(db_user.id, db_user.roles) ban_info = if Map.has_key?(db_user, :ban_expiration), do: %{ ban_expiration: db_user.ban_expiration }, else: %{} ban_info = if Map.has_key?(db_user, :malicious_score), do: Map.put(ban_info, :malicious_score, db_user.malicious_score) From f8c5e4fc8c361e10b89db0da07407bde42129512 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 27 Sep 2022 11:48:28 -1000 Subject: [PATCH 155/231] refactor(auth-view): credentials.json -> user.json --- .../controllers/auth_controller.ex | 6 +++-- lib/epochtalk_server_web/views/auth_view.ex | 22 +++---------------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index 62a40ca5..409fa8e0 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -52,8 +52,9 @@ defmodule EpochtalkServerWeb.AuthController do user = Map.put(user, :token, token) conn - |> render("credentials.json", user: user) + |> render("user.json", user: user) end + def logout(conn, _attrs) do if Guardian.Plug.authenticated?(conn) do # TODO: check if user is on page that requires auth @@ -64,6 +65,7 @@ defmodule EpochtalkServerWeb.AuthController do raise(NotLoggedIn) end end + def login(conn, user_params) when not is_map_key(user_params, "rememberMe") do login(conn, Map.put(user_params, "rememberMe", false)) end @@ -97,7 +99,7 @@ defmodule EpochtalkServerWeb.AuthController do # reply with user data conn - |> render("credentials.json", user: user) + |> render("user.json", user: user) end end end diff --git a/lib/epochtalk_server_web/views/auth_view.ex b/lib/epochtalk_server_web/views/auth_view.ex index 10421971..25437d40 100644 --- a/lib/epochtalk_server_web/views/auth_view.ex +++ b/lib/epochtalk_server_web/views/auth_view.ex @@ -2,26 +2,10 @@ defmodule EpochtalkServerWeb.AuthView do use EpochtalkServerWeb, :view alias EpochtalkServer.Models.Role - def render("search.json", %{found: found}) do - %{found: found} - end + def render("search.json", %{found: found}), do: %{found: found} def render("logout.json", _data), do: true - def render("show.json", %{user: user}) do - user |> user_json() - end - def render("credentials.json", %{user: user}) do - user |> format_user_reply() - end - def user_json(user) do - %{ - id: user.id, - username: user.username, - # TODO(boka): fill in these fields - avatar: "", # user.avatar - permissions: %{}, # user.permissions - roles: %{} # user.roles - } - end + def render("user.json", %{user: user}), do: format_user_reply(user) + defp format_user_reply(user) do reply = %{ token: user.token, From db1f89804b65aa95e0e0fe36f03b3ac3f3279cbd Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 27 Sep 2022 11:49:35 -1000 Subject: [PATCH 156/231] feat(remote-ip): add remote-ip package to fetch request ip even when behind ngnix or other proxy --- lib/epochtalk_server_web/endpoint.ex | 2 ++ mix.exs | 1 + mix.lock | 2 ++ 3 files changed, 5 insertions(+) diff --git a/lib/epochtalk_server_web/endpoint.ex b/lib/epochtalk_server_web/endpoint.ex index e61fd192..40aecbf0 100644 --- a/lib/epochtalk_server_web/endpoint.ex +++ b/lib/epochtalk_server_web/endpoint.ex @@ -1,5 +1,7 @@ defmodule EpochtalkServerWeb.Endpoint do use Phoenix.Endpoint, otp_app: :epochtalk_server + # get x-forwarded ip + plug RemoteIp # cors configuration plug Corsica, origins: "*", allow_headers: :all diff --git a/mix.exs b/mix.exs index ff20732d..275ebcc6 100644 --- a/mix.exs +++ b/mix.exs @@ -46,6 +46,7 @@ defmodule EpochtalkServer.MixProject do {:plug_cowboy, "~> 2.5"}, {:postgrex, ">= 0.0.0"}, {:redix, "~> 1.1.5"}, + {:remote_ip, "~> 1.0.0"}, {:swoosh, "~> 1.3"}, {:telemetry_metrics, "~> 0.6"}, {:telemetry_poller, "~> 1.0"}, diff --git a/mix.lock b/mix.lock index 94b906ec..1484450a 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,6 @@ %{ "argon2_elixir": {:hex, :argon2_elixir, "3.0.0", "fd4405f593e77b525a5c667282172dd32772d7c4fa58cdecdaae79d2713b6c5f", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "8b753b270af557d51ba13fcdebc0f0ab27a2a6792df72fd5a6cf9cfaffcedc57"}, + "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "comeonin": {:hex, :comeonin, "5.3.3", "2c564dac95a35650e9b6acfe6d2952083d8a08e4a89b93a481acb552b325892e", [:mix], [], "hexpm", "3e38c9c2cb080828116597ca8807bb482618a315bfafd98c90bc22a821cc84df"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, "corsica": {:hex, :corsica, "1.2.0", "5774cb77fd1d66ab89ffc2f04b2249f8e386bc37790a9f4bf101330ca247c02d", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c71f870555ce7a3eded55bbe937234cc48c546e73ce75745df9f59531687a759"}, @@ -28,6 +29,7 @@ "postgrex": {:hex, :postgrex, "0.16.4", "26d998467b4a22252285e728a29d341e08403d084e44674784975bb1cd00d2cb", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "3234d1a70cb7b1e0c95d2e242785ec2a7a94a092bbcef4472320b950cfd64c5f"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "redix": {:hex, :redix, "1.1.5", "6fc460d66a5c2287e83e6d73dddc8d527ff59cb4d4f298b41e03a4db8c3b2bd5", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "679afdd4c14502fe9c11387ff1cdcb33065a1cf511097da1eee407f17c7a418b"}, + "remote_ip": {:hex, :remote_ip, "1.0.0", "3d7fb45204a5704443f480cee9515e464997f52c35e0a60b6ece1f81484067ae", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9e9fcad4e50c43b5234bb6a9629ed6ab223f3ed07147bd35470e4ee5c8caf907"}, "swoosh": {:hex, :swoosh, "1.7.4", "f967d9b2659e81bab241b96267aae1001d35c2beea2df9c03dcf47b007bf566f", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1553d994b4cf069162965e63de1e1c53d8236e127118d21e56ce2abeaa3f25b4"}, "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, From f6b10fb0aa6a81dbaec4c60c3b2306aacb75b365 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 27 Sep 2022 15:00:32 -1000 Subject: [PATCH 157/231] fix(session): resolve issue storing ban_info and moderating --- lib/epochtalk_server/auth/guardian.ex | 3 ++- lib/epochtalk_server/session.ex | 16 ++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/epochtalk_server/auth/guardian.ex b/lib/epochtalk_server/auth/guardian.ex index a5d757f9..e5aa4a9f 100644 --- a/lib/epochtalk_server/auth/guardian.ex +++ b/lib/epochtalk_server/auth/guardian.ex @@ -37,7 +37,8 @@ defmodule EpochtalkServer.Auth.Guardian do roles: Redix.command!(:redix, ["SMEMBERS", "user:#{user_id}:roles"]) |> Role.by_lookup } # only append moderating, ban_expiration and malicious_score if present - resource = if length(moderating = Redix.command!(:redix, ["SMEMBERS", "user:#{user_id}:moderating"])) != 0, + moderating = Redix.command!(:redix, ["SMEMBERS", "user:#{user_id}:moderating"]) + resource = if moderating && length(moderating) != 0, do: Map.put(resource, :moderating, moderating), else: resource resource = if (ban_expiration = Redix.command!(:redix, ["HEXISTS", "user:#{user_id}:ban_info", "ban_expiration"])) != 0, do: Map.put(resource, :ban_expiration, ban_expiration), else: resource diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index 018f85a8..5ef049e9 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -33,10 +33,10 @@ defmodule EpochtalkServer.Session do end defp save(db_user, session_id) do # TODO: return role lookups from db instead of entire roles - update_user_info(db_user.id, db_user.username, Map.get(db_user,:avatar)) + update_user_info(db_user.id, db_user.username, Map.get(db_user, :avatar)) update_roles(db_user.id, db_user.roles) ban_info = if Map.has_key?(db_user, :ban_expiration), do: %{ ban_expiration: db_user.ban_expiration }, else: %{} - ban_info = if Map.has_key?(db_user, :malicious_score), do: Map.put(ban_info, :malicious_score, db_user.malicious_score) + ban_info = if Map.has_key?(db_user, :malicious_score), do: Map.put(ban_info, :malicious_score, db_user.malicious_score), else: ban_info update_ban_info(db_user.id, ban_info) update_moderating(db_user.id, Map.get(db_user, :moderating)) set_session(db_user.id, session_id) @@ -48,16 +48,15 @@ defmodule EpochtalkServer.Session do |> Enum.map(&(&1.lookup)) role_key = generate_key(user_id, "roles") Redix.command(:redix, ["DEL", role_key]) - role_lookups - |> Enum.each(&Redix.command(:redix, ["SADD", role_key, &1])) + unless role_lookups == nil or role_lookups == [], do: + Enum.each(role_lookups, &Redix.command(:redix, ["SADD", role_key, &1])) end def update_moderating(user_id, moderating) do # save/replace moderating boards to redis under "user:{user_id}:moderating" moderating_key = generate_key(user_id, "moderating") Redix.command(:redix, ["DEL", moderating_key]) - unless moderating == nil or moderating == [] do - Redix.command(:redix, ["SADD", moderating_key, moderating]) - end + unless moderating == nil or moderating == [], do: + Enum.each(moderating, &Redix.command(:redix, ["SADD", moderating_key, &1])) end def update_user_info(user_id, username) do user_key = generate_key(user_id, "user") @@ -78,7 +77,8 @@ defmodule EpochtalkServer.Session do # save/replace ban_expiration to redis under "user:{user_id}:baninfo" ban_key = generate_key(user_id, "baninfo") Redix.command(:redix, ["HDEL", ban_key, "ban_expiration", "malicious_score"]) - Redix.command(:redix, ["HSET", ban_key, ban_info]) + if ban_exp = Map.get(ban_info, :ban_expiration), do: Redix.command(:redix, ["HSET", ban_key, "ban_expiration", ban_exp]) + if malicious_score = Map.get(ban_info, :malicious_score), do: Redix.command(:redix, ["HSET", ban_key, "malicious_score", malicious_score]) end defp set_session(user_id, session_id) do # save session id to redis under "user:{user_id}:sessions" From 94aecadb9344bc74eab041602fbb99aebcec5826 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 29 Sep 2022 11:58:29 -1000 Subject: [PATCH 158/231] feat(error-handling): standardize error handling with format_error method, handles errors from guardian and raises --- .../controllers/auth_controller.ex | 12 ++--- lib/epochtalk_server_web/endpoint.ex | 6 ++- .../plugs/prepare_parse.ex | 50 +++++++++++++++++++ .../views/custom_errors.ex | 8 +++ .../views/error_helpers.ex | 22 +++++--- lib/epochtalk_server_web/views/error_view.ex | 30 +++++++---- .../views/guardian_error_handler.ex | 2 +- 7 files changed, 103 insertions(+), 27 deletions(-) create mode 100644 lib/epochtalk_server_web/plugs/prepare_parse.ex diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index 409fa8e0..e60423c7 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -11,7 +11,8 @@ defmodule EpochtalkServerWeb.AuthController do InvalidCredentials, NotLoggedIn, AccountNotConfirmed, - AccountMigrationNotComplete + AccountMigrationNotComplete, + InvalidPayload } alias EpochtalkServerWeb.ErrorView @@ -41,13 +42,7 @@ defmodule EpochtalkServerWeb.AuthController do |> render("400.json", %{message: inspect(changeset.errors)}) end end - def authenticate(conn), do: authenticate(conn, %{}) - def authenticate(conn, _attrs) do - user = conn - |> Guardian.Plug.current_resource - - token = conn - |> Guardian.Plug.current_token + def register(_conn, _attrs), do: raise(InvalidPayload) user = Map.put(user, :token, token) @@ -102,4 +97,5 @@ defmodule EpochtalkServerWeb.AuthController do |> render("user.json", user: user) end end + def login(_conn, _attrs), do: raise(InvalidPayload) end diff --git a/lib/epochtalk_server_web/endpoint.ex b/lib/epochtalk_server_web/endpoint.ex index 40aecbf0..cb4431a1 100644 --- a/lib/epochtalk_server_web/endpoint.ex +++ b/lib/epochtalk_server_web/endpoint.ex @@ -27,13 +27,17 @@ defmodule EpochtalkServerWeb.Endpoint do plug Plug.RequestId plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] - + plug EpochtalkServerWeb.Plugs.PrepareParse # handle malformed json payload plug Plug.Parsers, parsers: [:urlencoded, :multipart, :json], pass: ["*/*"], + body_reader: {CacheBodyReader, :read_body, []}, json_decoder: Phoenix.json_library() plug Plug.MethodOverride plug Plug.Head plug EpochtalkServerWeb.Router end + +# used to help preparse raw req body, in case of malformed payload +defmodule CacheBodyReader, do: def read_body(conn, _opts), do: {:ok, conn.assigns.raw_body, conn} diff --git a/lib/epochtalk_server_web/plugs/prepare_parse.ex b/lib/epochtalk_server_web/plugs/prepare_parse.ex new file mode 100644 index 00000000..015dfb70 --- /dev/null +++ b/lib/epochtalk_server_web/plugs/prepare_parse.ex @@ -0,0 +1,50 @@ +defmodule EpochtalkServerWeb.Plugs.PrepareParse do + import Plug.Conn + use Phoenix.Controller + alias EpochtalkServerWeb.ErrorView + @env Application.get_env(:application_api, :env) + @methods ~w(POST PUT PATCH) + + def init(opts), do: opts + + def call(conn, opts) do + %{method: method} = conn + + if method in @methods and not (@env in [:test]) do + case Plug.Conn.read_body(conn, opts) do + {:error, :timeout} -> + raise Plug.TimeoutError + + {:error, _} -> + raise Plug.BadRequestError + + {:more, _, conn} -> + # raise Plug.PayloadTooLargeError, conn: conn, router: __MODULE__ + error = %{message: "Payload too large error"} + render_error(conn, error) + + {:ok, "" = body, conn} -> + update_in(conn.assigns[:raw_body], &[body | &1 || []]) + + {:ok, body, conn} -> + case Jason.decode(body) do + {:ok, _result} -> + update_in(conn.assigns[:raw_body], &[body | &1 || []]) + + {:error, _reason} -> + error = %{message: "Malformed JSON in the body"} + render_error(conn, error) + end + end + else + conn + end + end + + def render_error(conn, %{ message: message } = _error) do + conn + |> put_status(400) + |> put_view(ErrorView) + |> render("400.json", message: message) + end +end diff --git a/lib/epochtalk_server_web/views/custom_errors.ex b/lib/epochtalk_server_web/views/custom_errors.ex index fce99e55..2c226cde 100644 --- a/lib/epochtalk_server_web/views/custom_errors.ex +++ b/lib/epochtalk_server_web/views/custom_errors.ex @@ -25,4 +25,12 @@ defmodule EpochtalkServerWeb.CustomErrors do """ defexception plug_status: 403, message: "User account migration not complete, please reset password", conn: nil, router: nil end + + # API parameter mismatch handling + defmodule InvalidPayload do + @moduledoc """ + Exception raised when api request payload is incorrect + """ + defexception plug_status: 400, message: "Invalid payload, check that the request payload is correct", conn: nil, router: nil + end end diff --git a/lib/epochtalk_server_web/views/error_helpers.ex b/lib/epochtalk_server_web/views/error_helpers.ex index b1c9177a..03cef042 100644 --- a/lib/epochtalk_server_web/views/error_helpers.ex +++ b/lib/epochtalk_server_web/views/error_helpers.ex @@ -4,13 +4,23 @@ defmodule EpochtalkServerWeb.ErrorHelpers do """ @doc """ - Translates an error message. + Translates changeset errors to string message. """ - def translate_error({msg, opts}) do - # Because the error messages we show in our forms and APIs - # are defined inside Ecto, we need to translate them dynamically. - Enum.reduce(opts, msg, fn {key, value}, acc -> - String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end) + def changeset_error_to_string(%Ecto.Changeset{} = changeset) do + Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} -> + Enum.reduce(opts, msg, fn {key, value}, acc -> + String.replace(acc, "%{#{key}}", _to_string(value)) + end) + end) + |> Enum.reduce("", fn {k, v}, acc -> + joined_errors = Enum.join(v, "; ") + if String.length(acc) < 1, + do: String.capitalize("#{k}") <> " #{joined_errors}", + else: "#{acc}, #{k} #{joined_errors}" end) end + def changeset_error_to_string(changeset), do: changeset + + defp _to_string(val) when is_list(val), do: Enum.join(val, ",") + defp _to_string(val), do: to_string(val) end diff --git a/lib/epochtalk_server_web/views/error_view.ex b/lib/epochtalk_server_web/views/error_view.ex index 1b9c3deb..1931d22d 100644 --- a/lib/epochtalk_server_web/views/error_view.ex +++ b/lib/epochtalk_server_web/views/error_view.ex @@ -4,20 +4,28 @@ defmodule EpochtalkServerWeb.ErrorView do # By default, Phoenix returns the status message from # the template name. For example, "404.json" becomes # "Not Found". - def template_not_found(template, assigns) do - %{ - status: assigns.status, - message: assigns.reason.message, - error: Phoenix.Controller.status_message_from_template(template) - } + def template_not_found(_template, assigns) do + format_error(assigns) + end + + # match assigns.reason.message and assigns.conn.status + defp format_error(%{reason: %{message: message}, conn: %{status: status}}), do: format_error(status, message) + # match assigns.reason.message and assigns.status + defp format_error(%{reason: %{message: message}, status: status}), do: format_error(status, message) + # match assigns.message and assigns.conn.status + defp format_error(%{message: message, conn: %{status: status}}), do: format_error(status, message) + # match assigns.message and assigns.status + defp format_error(%{message: message, status: status}), do: format_error(status, message) + # match stack and status (Ex: function error from redis in session) + defp format_error(%{stack: stack, status: status}) do + format_error(status, "Something went wrong") end - # Handles auth errors coming from guardian - def render("Guardian401.json", assigns) do + defp format_error(status, message) when is_binary(message) do %{ - status: assigns.conn.status, - message: assigns.message, - error: Phoenix.Controller.status_message_from_template(to_string(assigns.conn.status)) + status: status, + message: message, + error: Phoenix.Controller.status_message_from_template(to_string(status)) } end end diff --git a/lib/epochtalk_server_web/views/guardian_error_handler.ex b/lib/epochtalk_server_web/views/guardian_error_handler.ex index 40ee8ead..c6834ef4 100644 --- a/lib/epochtalk_server_web/views/guardian_error_handler.ex +++ b/lib/epochtalk_server_web/views/guardian_error_handler.ex @@ -11,6 +11,6 @@ defmodule EpochtalkServerWeb.GuardianErrorHandler do conn |> put_status(401) |> put_view(ErrorView) - |> render("Guardian401.json", message: message) + |> render("401.json", message: message) end end From 3250ca0ac064af56b3202b8def44106ad1beade6 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 29 Sep 2022 11:59:54 -1000 Subject: [PATCH 159/231] fix(error-handling): add missing alias --- lib/epochtalk_server_web/controllers/auth_controller.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index e60423c7..4f927627 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -15,6 +15,7 @@ defmodule EpochtalkServerWeb.AuthController do InvalidPayload } alias EpochtalkServerWeb.ErrorView + alias EpochtalkServerWeb.ErrorHelpers def username(conn, %{"username" => username}) do username_found = username From c043a126977b95ffd475ae3260daf134a3d687b5 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 29 Sep 2022 14:59:50 -1000 Subject: [PATCH 160/231] feat(bans): implement ban(%User{}), bans user appends roles and ban_expiration. rename existing ban -> ban_by_user_id --- lib/epochtalk_server/models/ban.ex | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/epochtalk_server/models/ban.ex b/lib/epochtalk_server/models/ban.ex index fe8572e0..426aef43 100644 --- a/lib/epochtalk_server/models/ban.ex +++ b/lib/epochtalk_server/models/ban.ex @@ -49,8 +49,20 @@ defmodule EpochtalkServer.Models.Ban do end def by_user_id(user_id) when is_integer(user_id), do: Repo.get_by(Ban, user_id: user_id) - def ban(user_id), do: ban(user_id, nil) - def ban(user_id, expiration) do + def ban(%User{} = user), do: ban(user, nil) + def ban(%User{ id: id} = user, expiration) do + case ban_by_user_id(id, expiration) do + {:ok, ban_info} -> # successful ban, update roles/ban info on user + user = user + |> Map.put(:roles, Role.by_user_id(id)) + |> Map.put(:ban_expiration, ban_info.expiration) + {:ok, user} + {:error, err} -> # print error, return human readable message + IO.inspect err + {:error, "There was an issue banning user"} + end + end + defp ban_by_user_id(user_id, expiration) do Repo.transaction(fn -> RoleUser.set_user_role(Role.get_banned_role_id, user_id) case Repo.get_by(Ban, user_id: user_id) do @@ -58,7 +70,6 @@ defmodule EpochtalkServer.Models.Ban do ban -> ban_changeset(ban, %{user_id: user_id, expiration: expiration}) end |> Repo.insert_or_update! - |> Map.put(:roles, Role.by_user_id(user_id)) end) end From 85c627a24b743ef0b0955f69b4344dcd10c10040 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 29 Sep 2022 15:01:51 -1000 Subject: [PATCH 161/231] refactor(user-malicious-score): move logic for banning user if malicious to User.handle_malicious_user --- lib/epochtalk_server/models/user.ex | 51 ++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/lib/epochtalk_server/models/user.ex b/lib/epochtalk_server/models/user.ex index dbed2930..eeb62050 100644 --- a/lib/epochtalk_server/models/user.ex +++ b/lib/epochtalk_server/models/user.ex @@ -9,6 +9,7 @@ defmodule EpochtalkServer.Models.User do alias EpochtalkServer.Models.Role alias EpochtalkServer.Models.RoleUser alias EpochtalkServer.Models.BannedAddress + alias EpochtalkServer.Models.Ban schema "users" do field :email, :string @@ -38,32 +39,52 @@ defmodule EpochtalkServer.Models.User do |> validate_password() end # create admin, for seeding - def create(user_attrs, true = _admin) do + def create(attrs, true = _admin) do Repo.transaction(fn -> - create(user_attrs) + create(attrs) |> case do {:ok, %{ id: id } = _user} -> RoleUser.set_admin(id) end end) end - # create user, for seeding - def create(user_attrs) do - %User{} - |> User.registration_changeset(user_attrs) - |> Repo.insert + def create(attrs) do + user_cs = User.registration_changeset(%User{}, attrs) + case Repo.insert(user_cs) do + {:ok, user} -> {:ok, user |> Map.put(:roles, Role.by_user_id(user.id))} # append roles + {:error, err} -> {:error, err} + end end def with_username_exists?(username), do: Repo.exists?(from u in User, where: u.username == ^username) + def with_email_exists?(email), do: Repo.exists?(from u in User, where: u.email == ^email) + def by_id(id) when is_integer(id), do: Repo.get_by(User, id: id) - defp set_malicious_score(id, value) do - from(u in User, where: u.id == ^id) - |> Repo.update_all(set: [malicious_score: value]) + + defp set_malicious_score(%User{ id: id } = user, malicious_score) do + from(u in User, where: u.id == ^id) + |> Repo.update_all(set: [malicious_score: malicious_score]) + if malicious_score != nil && malicious_score >= 1, + do: user |> Map.put(:malicious_score, malicious_score), + else: user end - def clear_malicious_score(id) when is_integer(id), do: set_malicious_score(id, nil) - def get_and_set_malicious_score(id, ip) when is_integer(id) and is_binary(ip) do - malicious_score = BannedAddress.calculate_malicious_score_from_ip(ip) - set_malicious_score(id, malicious_score) - malicious_score + + def clear_malicious_score(%User{} = user), do: set_malicious_score(user, nil) + + def handle_malicious_user(%User{} = user, ip) do + # convert ip tuple into string + ip_str = ip |> :inet_parse.ntoa |> to_string + # calculate user's malicious score from ip + malicious_score = BannedAddress.calculate_malicious_score_from_ip(ip_str) + # set user's malicious score + user = set_malicious_score(user, malicious_score) + # if user's malicious score is 1 or more ban the user, update roles and ban info + user = ban_if_malicious(user) + user end + + defp ban_if_malicious(%User{ malicious_score: nil } = user), do: {:ok, user} + defp ban_if_malicious(%User{ malicious_score: score } = user) when score < 1, do: {:ok, user} + defp ban_if_malicious(%User{ malicious_score: score } = user) when score >= 1, do: Ban.ban(user) + def by_username(username) when is_binary(username) do query = from u in User, left_join: p in Profile, From c6fcf28c355e5f531f78e08bc83acd8f2ad6d0aa Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 29 Sep 2022 15:04:21 -1000 Subject: [PATCH 162/231] refactor(auth): code cleanup authenticate, login function headers --- .../controllers/auth_controller.ex | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index 4f927627..66f608b1 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -47,8 +47,11 @@ defmodule EpochtalkServerWeb.AuthController do user = Map.put(user, :token, token) - conn - |> render("user.json", user: user) + def authenticate(conn), do: authenticate(conn, nil) + def authenticate(conn, _attrs) do + token = Guardian.Plug.current_token(conn) + user = Guardian.Plug.current_resource(conn) |> Map.put(:token, token) + render(conn, "user.json", user: user) end def logout(conn, _attrs) do @@ -62,10 +65,10 @@ defmodule EpochtalkServerWeb.AuthController do end end - def login(conn, user_params) when not is_map_key(user_params, "rememberMe") do - login(conn, Map.put(user_params, "rememberMe", false)) + def login(conn, attrs) when not is_map_key(attrs, "rememberMe") do + login(conn, Map.put(attrs, "rememberMe", false)) end - def login(conn, %{"username" => username, "password" => password} = _user_params) do + def login(conn, %{"username" => username, "password" => password} = _attrs) do if Guardian.Plug.authenticated?(conn) do authenticate(conn) else From 109f621c81520bab774c34155866b675a19aec25 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 29 Sep 2022 15:05:07 -1000 Subject: [PATCH 163/231] refactor(user-reply): only append moderating list if has values --- lib/epochtalk_server_web/views/auth_view.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/epochtalk_server_web/views/auth_view.ex b/lib/epochtalk_server_web/views/auth_view.ex index 25437d40..7e237b24 100644 --- a/lib/epochtalk_server_web/views/auth_view.ex +++ b/lib/epochtalk_server_web/views/auth_view.ex @@ -16,7 +16,8 @@ defmodule EpochtalkServerWeb.AuthView do roles: Enum.map(user.roles, &(&1.lookup)) } # only append moderating, ban_expiration and malicious_score if present - reply = if length(moderating = Map.get(user, :moderating)) != 0, + moderating = Map.get(user, :moderating) + reply = if !is_nil(moderating) && length(moderating) != 0, do: Map.put(reply, :moderating, moderating), else: reply reply = if exp = Map.get(user, :ban_expiration), From c2976703fc1f9a21c90000a427b023322244c167 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 29 Sep 2022 15:08:40 -1000 Subject: [PATCH 164/231] feat(error-handling): format errors with unknown message, returns human readble json error, console will still display actual error --- lib/epochtalk_server_web/views/error_view.ex | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/epochtalk_server_web/views/error_view.ex b/lib/epochtalk_server_web/views/error_view.ex index 1931d22d..eb773ae3 100644 --- a/lib/epochtalk_server_web/views/error_view.ex +++ b/lib/epochtalk_server_web/views/error_view.ex @@ -4,9 +4,7 @@ defmodule EpochtalkServerWeb.ErrorView do # By default, Phoenix returns the status message from # the template name. For example, "404.json" becomes # "Not Found". - def template_not_found(_template, assigns) do - format_error(assigns) - end + def template_not_found(_template, assigns), do: format_error(assigns) # match assigns.reason.message and assigns.conn.status defp format_error(%{reason: %{message: message}, conn: %{status: status}}), do: format_error(status, message) @@ -17,10 +15,11 @@ defmodule EpochtalkServerWeb.ErrorView do # match assigns.message and assigns.status defp format_error(%{message: message, status: status}), do: format_error(status, message) # match stack and status (Ex: function error from redis in session) - defp format_error(%{stack: stack, status: status}) do + defp format_error(%{stack: _, status: status}) do format_error(status, "Something went wrong") end + # format errors with readable message defp format_error(status, message) when is_binary(message) do %{ status: status, @@ -28,4 +27,6 @@ defmodule EpochtalkServerWeb.ErrorView do error: Phoenix.Controller.status_message_from_template(to_string(status)) } end + # format errors with unknown error + defp format_error(status, data), do: format_error(status, "Something went wrong") end From 1752e0e03c22ceeebaa797a1d036b57832d8414b Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 29 Sep 2022 15:09:42 -1000 Subject: [PATCH 165/231] feat(error-handling): create helper method render_json_error to proccess all api controller errors --- lib/epochtalk_server_web/views/error_helpers.ex | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/epochtalk_server_web/views/error_helpers.ex b/lib/epochtalk_server_web/views/error_helpers.ex index 03cef042..a054b414 100644 --- a/lib/epochtalk_server_web/views/error_helpers.ex +++ b/lib/epochtalk_server_web/views/error_helpers.ex @@ -1,8 +1,22 @@ defmodule EpochtalkServerWeb.ErrorHelpers do + use Phoenix.Controller + alias EpochtalkServerWeb.ErrorView @moduledoc """ Conveniences for translating and building error messages. """ + @doc """ + Renders error json from error data which could be a message or changeset errors. + """ + def render_json_error(conn, status, %Ecto.Changeset{} = changeset), do: + render_json_error(conn, status, changeset_error_to_string(changeset)) + def render_json_error(conn, status, message) do + conn + |> put_status(status) + |> put_view(ErrorView) + |> render("#{status}.json", message: message) + end + @doc """ Translates changeset errors to string message. """ From fb4bcd03445df6a0f732afb8efe1c78856e81987 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 29 Sep 2022 15:10:43 -1000 Subject: [PATCH 166/231] refactor(guardian-error-handler): modify guardian error handler to user ErrorHelpers.render_json_error --- .../views/guardian_error_handler.ex | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/epochtalk_server_web/views/guardian_error_handler.ex b/lib/epochtalk_server_web/views/guardian_error_handler.ex index c6834ef4..d2eebf2d 100644 --- a/lib/epochtalk_server_web/views/guardian_error_handler.ex +++ b/lib/epochtalk_server_web/views/guardian_error_handler.ex @@ -1,16 +1,15 @@ defmodule EpochtalkServerWeb.GuardianErrorHandler do - use Phoenix.Controller - alias EpochtalkServerWeb.ErrorView + alias EpochtalkServerWeb.ErrorHelpers - # This is used to convert errors coming out of the guardian pipelines into a 401 - # renders error tuple from guardian pipeline into a json error response + @doc """ + This is used to convert errors coming out of the guardian pipelines into a 401 + renders error tuple from guardian pipeline into a json error response + """ @behaviour Guardian.Plug.ErrorHandler @impl Guardian.Plug.ErrorHandler def auth_error(conn, {type, _reason}, _opts) do + # convert type atom into human readable string (ex :invalid_token -> Invalid token) message = to_string(type) |> String.replace("_", " ") |> String.capitalize - conn - |> put_status(401) - |> put_view(ErrorView) - |> render("401.json", message: message) + ErrorHelpers.render_json_error(conn, 401, message) end end From 393a5053bd4458a6af03d9513791591b90049ce6 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 29 Sep 2022 15:13:39 -1000 Subject: [PATCH 167/231] fix(auth-controller): remove unused code left over from messed up git add patch --- lib/epochtalk_server_web/controllers/auth_controller.ex | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index 66f608b1..7224c685 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -45,8 +45,6 @@ defmodule EpochtalkServerWeb.AuthController do end def register(_conn, _attrs), do: raise(InvalidPayload) - user = Map.put(user, :token, token) - def authenticate(conn), do: authenticate(conn, nil) def authenticate(conn, _attrs) do token = Guardian.Plug.current_token(conn) From 58aab5294f3292507db47e11c432aa6f812c767b Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Fri, 30 Sep 2022 11:54:37 -1000 Subject: [PATCH 168/231] refactor(malicious-score): return nil if less than 1 --- lib/epochtalk_server/models/banned_address.ex | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/epochtalk_server/models/banned_address.ex b/lib/epochtalk_server/models/banned_address.ex index 83f9ef82..864fab55 100644 --- a/lib/epochtalk_server/models/banned_address.ex +++ b/lib/epochtalk_server/models/banned_address.ex @@ -109,14 +109,15 @@ defmodule EpochtalkServer.Models.BannedAddress do {:ok, ip} -> hostname_score = case :inet_res.gethostbyaddr(ip) do {:ok, host} -> hostname_from_host(host) |> calculate_hostname_score - {:error, _} -> 0 # no hostname found, return 0 for hostname score + {:error, _} -> nil # no hostname found, return nil for hostname score end ip32_score = calculate_ip32_score(ip) ip24_score = calculate_ip24_score(ip) ip16_score = calculate_ip16_score(ip) # calculate malicious score using all scores - hostname_score + ip32_score + 0.04 + ip24_score + 0.0016 + ip16_score - {:error, _} -> 0 # invalid ip address, return 0 for malicious score + malicious_score = hostname_score + ip32_score + 0.04 + ip24_score + 0.0016 + ip16_score + if malicious_score < 1, do: nil, else: malicious_score + {:error, _} -> nil # invalid ip address, return nil for malicious score end end From 93eb6f9d776da13c3494627831919da26c20eb8c Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Fri, 30 Sep 2022 11:55:52 -1000 Subject: [PATCH 169/231] fix(preferences): user relationship belongs_to fix --- lib/epochtalk_server/models/preference.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epochtalk_server/models/preference.ex b/lib/epochtalk_server/models/preference.ex index 4b0c547a..1776e354 100644 --- a/lib/epochtalk_server/models/preference.ex +++ b/lib/epochtalk_server/models/preference.ex @@ -6,7 +6,7 @@ defmodule EpochtalkServer.Models.Preference do @primary_key false @schema_prefix "users" schema "preferences" do - belongs_to :user_id, User, foreign_key: :id, type: :integer + belongs_to :user, User field :posts_per_page, :integer field :threads_per_page, :integer field :collapsed_categories, :map From a48e98547142512bf7a34e69ab0da333fde22494 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Fri, 30 Sep 2022 11:56:12 -1000 Subject: [PATCH 170/231] fix(profile): user relationship belongs_to fix --- lib/epochtalk_server/models/profile.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epochtalk_server/models/profile.ex b/lib/epochtalk_server/models/profile.ex index 86d3f66b..88b04044 100644 --- a/lib/epochtalk_server/models/profile.ex +++ b/lib/epochtalk_server/models/profile.ex @@ -5,7 +5,7 @@ defmodule EpochtalkServer.Models.Profile do @schema_prefix "users" schema "profiles" do - belongs_to :user_id, User + belongs_to :user, User field :avatar, :string field :position, :string field :signature, :string From c899f2876c4bd17a898b43dad588ca96565ba0d3 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Fri, 30 Sep 2022 11:57:21 -1000 Subject: [PATCH 171/231] feat(user-model): add ecto relationships as fields, in prep for refactor --- lib/epochtalk_server/models/user.ex | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server/models/user.ex b/lib/epochtalk_server/models/user.ex index eeb62050..3eae6163 100644 --- a/lib/epochtalk_server/models/user.ex +++ b/lib/epochtalk_server/models/user.ex @@ -9,6 +9,7 @@ defmodule EpochtalkServer.Models.User do alias EpochtalkServer.Models.Role alias EpochtalkServer.Models.RoleUser alias EpochtalkServer.Models.BannedAddress + alias EpochtalkServer.Models.BoardModerator alias EpochtalkServer.Models.Ban schema "users" do @@ -20,14 +21,19 @@ defmodule EpochtalkServer.Models.User do field :confirmation_token, :string field :reset_token, :string field :reset_expiration, :string - field :created_at, :naive_datetime field :imported_at, :naive_datetime field :updated_at, :naive_datetime field :deleted, :boolean, default: false field :malicious_score, :decimal - field :smf_member, :map, virtual: true + + # relation fields + has_one :preferences, Preference + has_one :profile, Profile + has_one :ban_info, Ban + has_many :roles, RoleUser + has_many :moderating, BoardModerator end def registration_changeset(user, attrs) do From a277b43024d15249793b10baa1bc5ee9656032db Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Fri, 30 Sep 2022 11:58:38 -1000 Subject: [PATCH 172/231] feat(by-username): add error handling --- lib/epochtalk_server/models/ban.ex | 54 ++++++++++++++----- .../models/board_moderator.ex | 9 +++- lib/epochtalk_server/models/user.ex | 22 ++++---- 3 files changed, 61 insertions(+), 24 deletions(-) diff --git a/lib/epochtalk_server/models/ban.ex b/lib/epochtalk_server/models/ban.ex index 426aef43..7a41bb1e 100644 --- a/lib/epochtalk_server/models/ban.ex +++ b/lib/epochtalk_server/models/ban.ex @@ -52,14 +52,12 @@ defmodule EpochtalkServer.Models.Ban do def ban(%User{} = user), do: ban(user, nil) def ban(%User{ id: id} = user, expiration) do case ban_by_user_id(id, expiration) do - {:ok, ban_info} -> # successful ban, update roles/ban info on user - user = user - |> Map.put(:roles, Role.by_user_id(id)) - |> Map.put(:ban_expiration, ban_info.expiration) + {:ok, _ban_info} -> # successful ban, update roles/ban info on user + user = user |> Repo.preload([:ban_info, roles: [:role]], force: true) {:ok, user} {:error, err} -> # print error, return human readable message IO.inspect err - {:error, "There was an issue banning user"} + {:error, :ban_error} end end defp ban_by_user_id(user_id, expiration) do @@ -73,23 +71,51 @@ defmodule EpochtalkServer.Models.Ban do end) end - def unban(user_id) when is_integer(user_id) do + defp unban_by_user_id(user_id) when is_integer(user_id) do Repo.transaction(fn -> RoleUser.delete_user_role(Role.get_banned_role_id, user_id) # delete ban role from user - User.clear_malicious_score(user_id) # clear user malicious score + User.clear_malicious_score_by_id(user_id) # clear user malicious score case Repo.get_by(Ban, user_id: user_id) do - nil -> %{ user_id: user_id } + nil -> {:ok, nil} cs -> Repo.update!(unban_changeset(cs, %{ user_id: user_id })) end # unban the user - |> Map.put(:roles, Role.by_user_id(user_id)) # append user roles end) end # unban if ban is expired, return user's roles - def unban_if_expired(%{ban_expiration: expiration, id: user_id} = user) do - if NaiveDateTime.compare(expiration, NaiveDateTime.utc_now) == :lt, - do: unban(user_id) |> Map.get(:roles), - else: user.roles + def unban_fixed(%User{ban_info: %Ban{expiration: expiration}, id: user_id} = user) do + if NaiveDateTime.compare(expiration, NaiveDateTime.utc_now) == :lt do + case unban_by_user_id(user_id) do + {:ok, _result} -> #successful unban, update user roles and ban_info + user = user |> Repo.preload([:ban_info, roles: [:role]], force: true) + {:ok, user} + {:error, err} -> # print error, return human readable message + IO.inspect err + {:error, :unban_error} + end + else + {:ok, user} + end + end + # unban with not ban_expiration, just return user do nothing + def unban_fixed(%User{id: _user_id} = user), do: {:ok, user} + + def unban(%{ban_expiration: expiration, id: user_id} = user) do + if NaiveDateTime.compare(expiration, NaiveDateTime.utc_now) == :lt do + case unban_by_user_id(user_id) do + {:ok, _result} -> #successful unban, append updated user roles + user = user + |> Map.put(:roles, Role.by_user_id(user_id)) + |> Map.delete(:ban_expiration) + {:ok, user} + {:error, err} -> # print error, return human readable message + IO.inspect err + {:error, :unban_error} + end + else + {:ok, user} + end end - def unban_if_expired(user), do: user.roles + # unban with not ban_expiration, just return user do nothing + def unban(%{id: _user_id} = user), do: {:ok, user} end diff --git a/lib/epochtalk_server/models/board_moderator.ex b/lib/epochtalk_server/models/board_moderator.ex index 9fc8e24b..cd0306df 100644 --- a/lib/epochtalk_server/models/board_moderator.ex +++ b/lib/epochtalk_server/models/board_moderator.ex @@ -19,9 +19,16 @@ defmodule EpochtalkServer.Models.BoardModerator do |> validate_required([:user_id, :board_id]) end - def get_boards(user_id) when is_integer(user_id) do + defp get_boards_by_user_id(user_id) when is_integer(user_id) do from(bm in BoardModerator, where: bm.user_id == ^user_id) |> Repo.all() |> Enum.map(&(&1.board_id)) end + + def get_boards(%{ id: user_id } = user) do + moderating = get_boards_by_user_id(user_id) + if length(moderating) > 1, + do: {:ok, Map.put(user, :moderating, moderating)}, + else: {:ok, user} + end end diff --git a/lib/epochtalk_server/models/user.ex b/lib/epochtalk_server/models/user.ex index 3eae6163..29cc97c0 100644 --- a/lib/epochtalk_server/models/user.ex +++ b/lib/epochtalk_server/models/user.ex @@ -136,15 +136,19 @@ defmodule EpochtalkServer.Models.User do """, u.id, u.id)}, where: u.username == ^username - if user = Repo.one(query) do - # set all user's roles - user = Map.put(user, :roles, Role.by_user_id(user.id)) - # set primary role info - primary_role = List.first(user[:roles]) - hc = Map.get(primary_role, :highlight_color) - Map.put(user, :role_name, primary_role.name) - |> Map.put(:role_highlight_color, (if hc, do: hc, else: "")) - |> format_user + case Repo.one(query) do + nil -> {:error, :user_not_found} + user -> + # set all user's roles + user = Map.put(user, :roles, Role.by_user_id(user.id)) + # set primary role info + primary_role = List.first(user[:roles]) + hc = Map.get(primary_role, :highlight_color) + user = user + |> Map.put(:role_name, primary_role.name) + |> Map.put(:role_highlight_color, (if hc, do: hc, else: "")) + |> format_user + {:ok, user} end end def valid_password?(%{passhash: hashed_password} = _user, password) From d67445ea7dd8771a67d5cdb4f1ac1135f9308039 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Fri, 30 Sep 2022 13:14:00 -1000 Subject: [PATCH 173/231] feat(auth-refactor): register now works, refactor Major refactor of how queries are written. Now using ecto style for queries instead of manually creating a map with an unknown structure. Passing ecto models around and getting them back is much more predictable than passing around maps that we cannot guarantee the structure of. By passing around models we consistently know what were inputting and outputting. Refactoring also makes error handling more predictable. All auth routes have been refactored to have consistent error handling now. Yolo commit, sorry... --- lib/epochtalk_server/models/role.ex | 2 +- lib/epochtalk_server/models/user.ex | 47 ++++-- lib/epochtalk_server/session.ex | 41 +++--- .../controllers/auth_controller.ex | 138 +++++++++--------- lib/epochtalk_server_web/views/auth_view.ex | 74 +++++++--- 5 files changed, 182 insertions(+), 120 deletions(-) diff --git a/lib/epochtalk_server/models/role.ex b/lib/epochtalk_server/models/role.ex index a04a5709..42a98fb5 100644 --- a/lib/epochtalk_server/models/role.ex +++ b/lib/epochtalk_server/models/role.ex @@ -56,7 +56,7 @@ defmodule EpochtalkServer.Models.Role do end |> handle_banned_user_role # if banned, only [ banned ] is returned for roles end - defp get_default(), do: by_lookup("user") + def get_default(), do: by_lookup("user") defp handle_banned_user_role(roles), do: if ban_role = reduce_ban_role(roles), do: [ban_role], else: roles defp reduce_ban_role([]), do: nil diff --git a/lib/epochtalk_server/models/user.ex b/lib/epochtalk_server/models/user.ex index 29cc97c0..9436a757 100644 --- a/lib/epochtalk_server/models/user.ex +++ b/lib/epochtalk_server/models/user.ex @@ -58,6 +58,17 @@ defmodule EpochtalkServer.Models.User do {:error, err} -> {:error, err} end end + def create_fixed(attrs) do + user_cs = User.registration_changeset(%User{}, attrs) + case Repo.insert(user_cs) do + {:ok, user} -> + user = user + |> Repo.preload([:preferences, :profile, :ban_info, :moderating]) # load associations + |> Map.put(:roles, [%RoleUser{role: Role.get_default()}]) # default to user role + {:ok, user} # load associations + {:error, err} -> {:error, err} + end + end def with_username_exists?(username), do: Repo.exists?(from u in User, where: u.username == ^username) @@ -66,30 +77,46 @@ defmodule EpochtalkServer.Models.User do def by_id(id) when is_integer(id), do: Repo.get_by(User, id: id) defp set_malicious_score(%User{ id: id } = user, malicious_score) do - from(u in User, where: u.id == ^id) - |> Repo.update_all(set: [malicious_score: malicious_score]) - if malicious_score != nil && malicious_score >= 1, + set_malicious_score_by_id(id, malicious_score) + if malicious_score != nil, do: user |> Map.put(:malicious_score, malicious_score), else: user end - def clear_malicious_score(%User{} = user), do: set_malicious_score(user, nil) + defp set_malicious_score_by_id(id, malicious_score) do + from(u in User, where: u.id == ^id) + |> Repo.update_all(set: [malicious_score: malicious_score]) + end + + def clear_malicious_score_by_id(id), do: set_malicious_score_by_id(id, nil) def handle_malicious_user(%User{} = user, ip) do # convert ip tuple into string ip_str = ip |> :inet_parse.ntoa |> to_string - # calculate user's malicious score from ip + # calculate user's malicious score from ip, nil if less than 1 malicious_score = BannedAddress.calculate_malicious_score_from_ip(ip_str) # set user's malicious score user = set_malicious_score(user, malicious_score) # if user's malicious score is 1 or more ban the user, update roles and ban info - user = ban_if_malicious(user) - user + if is_nil(user.malicious_score) || user.malicious_score < 1, + do: {:ok, user}, + else: Ban.ban(user) end - defp ban_if_malicious(%User{ malicious_score: nil } = user), do: {:ok, user} - defp ban_if_malicious(%User{ malicious_score: score } = user) when score < 1, do: {:ok, user} - defp ban_if_malicious(%User{ malicious_score: score } = user) when score >= 1, do: Ban.ban(user) + def by_username_fixed(username) when is_binary(username) do + query = from u in User, + where: u.username == ^username, + preload: [:preferences, :profile, :ban_info, :moderating, roles: [:role]] + user = Repo.one(query) + if user do + case length(user.roles) do + 0 -> {:ok, Map.put(user, :roles, [%RoleUser{role: Role.get_default()}])} + _ -> {:ok, user} + end + else + {:error, :user_not_found} + end + end def by_username(username) when is_binary(username) do query = from u in User, diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index 5ef049e9..abb09af8 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -1,57 +1,52 @@ defmodule EpochtalkServer.Session do alias EpochtalkServer.Auth.Guardian + alias EpochtalkServer.Models.User # set user session id, timestamp, ttl # log user in with Guardian to get token # save user session info to redis - # return user with token - def create(user, conn) do + # returns {:ok, user, token and conn} + def create(%User{} = user, remember_me, conn) do datetime = NaiveDateTime.utc_now session_id = UUID.uuid1() decoded_token = %{ user_id: user.id, session_id: session_id, timestamp: datetime } # set token expiration based on rememberMe - ttl = case Map.get(user, "rememberMe") do - # set longer expiration - "true" -> {4, :weeks} - # set default expiration - _ -> {1, :day} - end + ttl = if remember_me, do: {4, :weeks}, else: {1, :day} # sign user in and get encoded token conn = Guardian.Plug.sign_in(conn, decoded_token, %{}, ttl: ttl) encoded_token = Guardian.Plug.current_token(conn) - # add token to user - user = Map.put(user, :token, encoded_token) - # save session save(user, session_id) - + #TODO(akinsey): error handling # return user with token - {user, conn} + {:ok, user, encoded_token, conn} end - defp save(db_user, session_id) do + defp save(user, session_id) do # TODO: return role lookups from db instead of entire roles - update_user_info(db_user.id, db_user.username, Map.get(db_user, :avatar)) - update_roles(db_user.id, db_user.roles) - ban_info = if Map.has_key?(db_user, :ban_expiration), do: %{ ban_expiration: db_user.ban_expiration }, else: %{} - ban_info = if Map.has_key?(db_user, :malicious_score), do: Map.put(ban_info, :malicious_score, db_user.malicious_score), else: ban_info - update_ban_info(db_user.id, ban_info) - update_moderating(db_user.id, Map.get(db_user, :moderating)) - set_session(db_user.id, session_id) + avatar = if is_nil(user.profile), do: nil, else: user.profile.avatar + update_user_info(user.id, user.username, avatar) + update_roles(user.id, user.roles) + ban_info = if is_nil(user.ban_info), do: %{}, else: %{ ban_expiration: user.ban_info.expiration } + ban_info = if !is_nil(user.malicious_score) && user.malicious_score >= 1, do: Map.put(ban_info, :malicious_score, user.malicious_score), else: ban_info + update_ban_info(user.id, ban_info) + update_moderating(user.id, user.moderating) + set_session(user.id, session_id) end # use default role def update_roles(user_id, roles) when is_list(roles) do # save/replace roles to redis under "user:{user_id}:roles" - role_lookups = roles - |> Enum.map(&(&1.lookup)) + role_lookups = roles |> Enum.map(&(&1.role.lookup)) role_key = generate_key(user_id, "roles") Redix.command(:redix, ["DEL", role_key]) unless role_lookups == nil or role_lookups == [], do: Enum.each(role_lookups, &Redix.command(:redix, ["SADD", role_key, &1])) end def update_moderating(user_id, moderating) do + # get list of board ids from user.moderating + moderating = moderating |> Enum.map(&(&1.board_id)) # save/replace moderating boards to redis under "user:{user_id}:moderating" moderating_key = generate_key(user_id, "moderating") Redix.command(:redix, ["DEL", moderating_key]) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index 7224c685..ebabe44a 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -2,11 +2,11 @@ defmodule EpochtalkServerWeb.AuthController do use EpochtalkServerWeb, :controller alias EpochtalkServer.Models.User alias EpochtalkServer.Models.Ban - alias EpochtalkServer.Models.BoardModerator - alias EpochtalkServer.Repo + alias EpochtalkServer.Models.Invitation + # alias EpochtalkServer.Models.BoardModerator alias EpochtalkServer.Auth.Guardian alias EpochtalkServer.Session - + alias EpochtalkServerWeb.ErrorHelpers alias EpochtalkServerWeb.CustomErrors.{ InvalidCredentials, NotLoggedIn, @@ -14,33 +14,29 @@ defmodule EpochtalkServerWeb.AuthController do AccountMigrationNotComplete, InvalidPayload } - alias EpochtalkServerWeb.ErrorView - alias EpochtalkServerWeb.ErrorHelpers - - def username(conn, %{"username" => username}) do - username_found = username - |> User.with_username_exists? - render(conn, "search.json", found: username_found) - end - def email(conn, %{"email" => email}) do - email_found = email - |> User.with_email_exists? + def username(conn, %{"username" => username}), do: render(conn, "search.json", found: User.with_username_exists?(username)) + def email(conn, %{"email" => email}), do: render(conn, "search.json", found: User.with_email_exists?(email)) - render(conn, "search.json", found: email_found) - end - def register(conn, attrs) do - %User{} - |> User.registration_changeset(attrs) - |> Repo.insert() - |> case do - {:ok, user} -> - conn - |> render("show.json", user: user) - {:error, %Ecto.Changeset{} = changeset} -> - conn - |> put_view(ErrorView) - |> render("400.json", %{message: inspect(changeset.errors)}) + # TODO(akinsey): handle config.inviteOnly + # TODO(akinsey): handle config.newbieEnabled + # TODO(akinsey): Send confirmation email + def register(conn, %{"username" => _, "email" => _, "password" => _} = attrs) do + with {:auth, false} <- {:auth, Guardian.Plug.authenticated?(conn)}, + {:ok, user} <- User.create_fixed(attrs), + {_count, nil} <- Invitation.delete(user.email), + {:ok, user} <- User.handle_malicious_user(user, conn.remote_ip), + {:ok, user, token, conn} <- Session.create(user, false, conn) do + render(conn, "user.json", %{ user: user, token: token}) + else + # user already authenticated + {:auth, true} -> authenticate(conn) + # error banning in handle_malicious_user + {:error, :ban_error} -> ErrorHelpers.render_json_error(conn, 500, "There was an error banning malicious user, upon login") + # error in user.create + {:error, data} -> ErrorHelpers.render_json_error(conn, 400, data) + # Catch all for any other errors + _ -> ErrorHelpers.render_json_error(conn, 500, "There was an issue registering") end end def register(_conn, _attrs), do: raise(InvalidPayload) @@ -48,55 +44,59 @@ defmodule EpochtalkServerWeb.AuthController do def authenticate(conn), do: authenticate(conn, nil) def authenticate(conn, _attrs) do token = Guardian.Plug.current_token(conn) - user = Guardian.Plug.current_resource(conn) |> Map.put(:token, token) - render(conn, "user.json", user: user) + user = Guardian.Plug.current_resource(conn) + render(conn, "user.json", %{ user: user, token: token}) end + # TODO: check if user is on page that requires auth def logout(conn, _attrs) do - if Guardian.Plug.authenticated?(conn) do - # TODO: check if user is on page that requires auth - conn - |> Guardian.Plug.sign_out - |> render("logout.json") + with {:auth, true} <- {:auth, Guardian.Plug.authenticated?(conn)}, + conn <- Guardian.Plug.sign_out(conn) do + render(conn, "logout.json") else - raise(NotLoggedIn) + {:auth, false} -> raise(NotLoggedIn) + _ -> ErrorHelpers.render_json_error(conn, 500, "There was an issue signing out") end end - def login(conn, attrs) when not is_map_key(attrs, "rememberMe") do - login(conn, Map.put(attrs, "rememberMe", false)) - end - def login(conn, %{"username" => username, "password" => password} = _attrs) do - if Guardian.Plug.authenticated?(conn) do - authenticate(conn) + def login(conn, attrs) when not is_map_key(attrs, "rememberMe"), do: login(conn, Map.put(attrs, "rememberMe", false)) + # def login(conn, %{"username" => username, "password" => password, "rememberMe" => _} = _attrs) do + # with {:auth, false} <- {:auth, Guardian.Plug.authenticated?(conn)}, + # {:ok, user} <- User.by_username(username), + # {:not_confirmed, false} <- {:not_confirmed, !!Map.get(user, :confirmation_token)}, + # {:missing_passhash, true} <- {:missing_passhash, !!Map.get(user, :passhash)}, + # {:valid_password, true} <- {:valid_password, User.valid_password?(user, password)}, + # {:ok, user} <- Ban.unban(user), + # {:ok, user} <- BoardModerator.get_boards(user), + # {user, conn} <- Session.create(user, conn) do + # render(conn, "user.json", user: user) + # else + # {:auth, true} -> authenticate(conn) + # {:error, :user_not_found} -> raise(InvalidCredentials) + # {:not_confirmed, true} -> raise(AccountNotConfirmed) + # {:missing_passhash, false} -> raise(AccountMigrationNotComplete) + # {:valid_password, false} -> raise(InvalidCredentials) + # {:error, :unban_error} -> ErrorHelpers.render_json_error(conn, 500, "There was an issue unbanning user, upon login") + # _ -> ErrorHelpers.render_json_error(conn, 500, "There was an issue signing out") + # end + # end + def login(conn, %{"username" => username, "password" => password, "rememberMe" => remember_me} = _attrs) do + with {:auth, false} <- {:auth, Guardian.Plug.authenticated?(conn)}, + {:ok, user} <- User.by_username_fixed(username), + {:not_confirmed, false} <- {:not_confirmed, !!Map.get(user, :confirmation_token)}, + {:missing_passhash, true} <- {:missing_passhash, !!Map.get(user, :passhash)}, + {:valid_password, true} <- {:valid_password, User.valid_password?(user, password)}, + {:ok, user} <- Ban.unban_fixed(user), + {:ok, user, token, conn} <- Session.create(user, remember_me, conn) do + render(conn, "user.json", %{ user: user, token: token}) else - user = User.by_username(username) - - # check that user exists - if !user, do: raise(InvalidCredentials) - - # check confirmation token - if Map.get(user, :confirmation_token), do: raise(AccountNotConfirmed) - - # check user migration - # if passhash doesn't exist, account hasn't been fully migrated - if !Map.get(user, :passhash), do: raise(AccountMigrationNotComplete) - - # check password - if !User.valid_password?(user, password), do: raise(InvalidCredentials) - - # unban if ban is expired and update roles - user = Map.put(user, :roles, Ban.unban_if_expired(user)) - - # get user's moderated boards - user = Map.put(user, :moderating, BoardModerator.get_boards(user.id)) - - # create session - {user, conn} = Session.create(user, conn) - - # reply with user data - conn - |> render("user.json", user: user) + {:auth, true} -> authenticate(conn) + {:error, :user_not_found} -> raise(InvalidCredentials) + {:not_confirmed, true} -> raise(AccountNotConfirmed) + {:missing_passhash, false} -> raise(AccountMigrationNotComplete) + {:valid_password, false} -> raise(InvalidCredentials) + {:error, :unban_error} -> ErrorHelpers.render_json_error(conn, 500, "There was an issue unbanning user, upon login") + _ -> ErrorHelpers.render_json_error(conn, 500, "There was an issue while attempting to login") end end def login(_conn, _attrs), do: raise(InvalidPayload) diff --git a/lib/epochtalk_server_web/views/auth_view.ex b/lib/epochtalk_server_web/views/auth_view.ex index 7e237b24..397f568f 100644 --- a/lib/epochtalk_server_web/views/auth_view.ex +++ b/lib/epochtalk_server_web/views/auth_view.ex @@ -1,31 +1,71 @@ defmodule EpochtalkServerWeb.AuthView do use EpochtalkServerWeb, :view alias EpochtalkServer.Models.Role + alias EpochtalkServer.Models.User def render("search.json", %{found: found}), do: %{found: found} + def render("logout.json", _data), do: true - def render("user.json", %{user: user}), do: format_user_reply(user) - defp format_user_reply(user) do + def render("user.json", %{user: user, token: token}), do: format_user_reply(user, token) + + + # Format reply from User ecto schema + defp format_user_reply(%User{} = user, token) do + avatar = if !!user.profile, do: user.profile.avatar, else: nil + permissions = Role.get_masked_permissions(Enum.map(user.roles, &(&1.role))) + roles = Enum.map(user.roles, &(&1.role.lookup)) + moderating = if length(user.moderating) != 0, do: Enum.map(user.moderating, &(&1.board_id)), else: nil + ban_expiration = if !!user.ban_info, do: user.ban_info.expiration, else: nil + malicious_score = if !!user.malicious_score, do: user.malicious_score, else: nil + format_user_reply(user, token, {avatar, permissions, roles, moderating, ban_expiration, malicious_score}) + end + defp format_user_reply(user, token) do + avatar = Map.get(user, :avatar) + permissions = Role.get_masked_permissions(user.roles) + roles = Enum.map(user.roles, &(&1.lookup)) + moderating = if Map.get(user, :moderating) && length(user.moderating) != 0, do: user.moderating, else: nil + ban_expiration = Map.get(user, :ban_expiration) + malicious_score = Map.get(user, :malicious_score) + format_user_reply(user, token, {avatar, permissions, roles, moderating, ban_expiration, malicious_score}) + end + + defp format_user_reply(user, token, {avatar, permissions, roles, moderating, ban_expiration, malicious_score}) do reply = %{ - token: user.token, + token: token, id: user.id, username: user.username, - avatar: Map.get(user, :avatar), # user.avatar - permissions: Role.get_masked_permissions(user.roles), # user.permissions - roles: Enum.map(user.roles, &(&1.lookup)) + permissions: permissions, + roles: roles } - # only append moderating, ban_expiration and malicious_score if present - moderating = Map.get(user, :moderating) - reply = if !is_nil(moderating) && length(moderating) != 0, - do: Map.put(reply, :moderating, moderating), - else: reply - reply = if exp = Map.get(user, :ban_expiration), - do: Map.put(reply, :ban_expiration, exp), - else: reply - reply = if ms = Map.get(user, :malicious_score), - do: Map.put(reply, :malicious_score, ms), - else: reply + # only append avatar, moderating, ban_expiration and malicious_score if present + reply = if avatar, do: Map.put(reply, :avatar, avatar), else: reply + reply = if moderating, do: Map.put(reply, :moderating, moderating), else: reply + reply = if ban_expiration, do: Map.put(reply, :ban_expiration, ban_expiration), else: reply + reply = if malicious_score, do: Map.put(reply, :malicious_score, malicious_score), else: reply reply end + + # defp format_user_reply(user, token) do + # reply = %{ + # token: token, + # id: user.id, + # username: user.username, + # avatar: Map.get(user, :avatar), # user.avatar + # permissions: Role.get_masked_permissions(user.roles), # user.permissions + # roles: Enum.map(user.roles, &(&1.lookup)) + # } + # # only append moderating, ban_expiration and malicious_score if present + # moderating = Map.get(user, :moderating) + # reply = if !is_nil(moderating) && length(moderating) != 0, + # do: Map.put(reply, :moderating, moderating), + # else: reply + # reply = if exp = Map.get(user, :ban_expiration), + # do: Map.put(reply, :ban_expiration, exp), + # else: reply + # reply = if ms = Map.get(user, :malicious_score), + # do: Map.put(reply, :malicious_score, ms), + # else: reply + # reply + # end end From f8e6bc81313f55efbaf577fba036f4b5916dda1b Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Fri, 30 Sep 2022 21:13:01 -1000 Subject: [PATCH 174/231] refactor(code-cleanup): major refactor and reformatting of code, yolo... sorry... --- lib/epochtalk_server/models/ban.ex | 67 ++++---- lib/epochtalk_server/models/banned_address.ex | 26 +++- lib/epochtalk_server/models/board.ex | 6 + lib/epochtalk_server/models/board_mapping.ex | 5 + .../models/board_moderator.ex | 4 + lib/epochtalk_server/models/category.ex | 25 +-- lib/epochtalk_server/models/invitation.ex | 5 + lib/epochtalk_server/models/metadata_board.ex | 5 + lib/epochtalk_server/models/permission.ex | 15 +- lib/epochtalk_server/models/preference.ex | 2 + lib/epochtalk_server/models/profile.ex | 2 + lib/epochtalk_server/models/role.ex | 77 ++++++--- .../models/role_permission.ex | 7 + lib/epochtalk_server/models/role_user.ex | 6 + lib/epochtalk_server/models/user.ex | 146 +++++------------- lib/epochtalk_server/session.ex | 2 +- .../controllers/auth_controller.ex | 42 ++--- .../{views => errors}/custom_errors.ex | 3 + .../{views => errors}/error_helpers.ex | 3 +- .../guardian_error_handler.ex | 0 .../plugs/prepare_parse.ex | 32 +--- lib/epochtalk_server_web/views/auth_view.ex | 49 ++---- lib/epochtalk_server_web/views/error_view.ex | 7 +- 23 files changed, 247 insertions(+), 289 deletions(-) rename lib/epochtalk_server_web/{views => errors}/custom_errors.ex (99%) rename lib/epochtalk_server_web/{views => errors}/error_helpers.ex (98%) rename lib/epochtalk_server_web/{views => errors}/guardian_error_handler.ex (100%) diff --git a/lib/epochtalk_server/models/ban.ex b/lib/epochtalk_server/models/ban.ex index 7a41bb1e..41d7adb0 100644 --- a/lib/epochtalk_server/models/ban.ex +++ b/lib/epochtalk_server/models/ban.ex @@ -19,6 +19,8 @@ defmodule EpochtalkServer.Models.Ban do field :updated_at, :naive_datetime end + ## === Changesets Functions === + def changeset(ban, attrs \\ %{}) do ban |> cast(attrs, [:id, :user_id, :expiration, :created_at, :updated_at]) @@ -47,20 +49,12 @@ defmodule EpochtalkServer.Models.Ban do |> cast(attrs, [:user_id, :expiration, :updated_at]) |> validate_required([:user_id]) end + + ## === Database Functions === + def by_user_id(user_id) when is_integer(user_id), do: Repo.get_by(Ban, user_id: user_id) - def ban(%User{} = user), do: ban(user, nil) - def ban(%User{ id: id} = user, expiration) do - case ban_by_user_id(id, expiration) do - {:ok, _ban_info} -> # successful ban, update roles/ban info on user - user = user |> Repo.preload([:ban_info, roles: [:role]], force: true) - {:ok, user} - {:error, err} -> # print error, return human readable message - IO.inspect err - {:error, :ban_error} - end - end - defp ban_by_user_id(user_id, expiration) do + def ban_by_user_id(user_id, expiration) do Repo.transaction(fn -> RoleUser.set_user_role(Role.get_banned_role_id, user_id) case Repo.get_by(Ban, user_id: user_id) do @@ -71,7 +65,23 @@ defmodule EpochtalkServer.Models.Ban do end) end - defp unban_by_user_id(user_id) when is_integer(user_id) do + # Ban without expiration, is a permanent ban, expiration nil + def ban(%User{} = user), do: ban(user, nil) + # Ban has an expiration + def ban(%User{ id: id} = user, expiration) do + case ban_by_user_id(id, expiration) do + {:ok, _ban_info} -> # successful ban, update roles/ban info on user + user = user + |> Repo.preload([:ban_info, :roles], force: true) + |> Role.handle_banned_user_role() # only return banned role inside roles once user is banned + {:ok, user} + {:error, err} -> # print error, return error atom + IO.inspect err + {:error, :ban_error} + end + end + + def unban_by_user_id(user_id) when is_integer(user_id) do Repo.transaction(fn -> RoleUser.delete_user_role(Role.get_banned_role_id, user_id) # delete ban role from user User.clear_malicious_score_by_id(user_id) # clear user malicious score @@ -83,32 +93,17 @@ defmodule EpochtalkServer.Models.Ban do end # unban if ban is expired, return user's roles - def unban_fixed(%User{ban_info: %Ban{expiration: expiration}, id: user_id} = user) do + def unban(%User{ban_info: %Ban{expiration: expiration}, id: user_id} = user) do if NaiveDateTime.compare(expiration, NaiveDateTime.utc_now) == :lt do case unban_by_user_id(user_id) do {:ok, _result} -> #successful unban, update user roles and ban_info - user = user |> Repo.preload([:ban_info, roles: [:role]], force: true) - {:ok, user} - {:error, err} -> # print error, return human readable message - IO.inspect err - {:error, :unban_error} - end - else - {:ok, user} - end - end - # unban with not ban_expiration, just return user do nothing - def unban_fixed(%User{id: _user_id} = user), do: {:ok, user} - - def unban(%{ban_expiration: expiration, id: user_id} = user) do - if NaiveDateTime.compare(expiration, NaiveDateTime.utc_now) == :lt do - case unban_by_user_id(user_id) do - {:ok, _result} -> #successful unban, append updated user roles user = user - |> Map.put(:roles, Role.by_user_id(user_id)) - |> Map.delete(:ban_expiration) + |> Repo.preload([:roles], force: true) + |> Role.handle_empty_user_roles() # check if roles are empty after unbanning + |> Map.put(:malicious_score, nil) # clear malicious score + |> Map.put(:ban_info, nil) # clear ban info so session gets updated {:ok, user} - {:error, err} -> # print error, return human readable message + {:error, err} -> # print error, return error atom IO.inspect err {:error, :unban_error} end @@ -116,6 +111,6 @@ defmodule EpochtalkServer.Models.Ban do {:ok, user} end end - # unban with not ban_expiration, just return user do nothing - def unban(%{id: _user_id} = user), do: {:ok, user} + # unban with no ban_expiration, just output user do nothing + def unban(%User{id: _user_id} = user), do: {:ok, user} end diff --git a/lib/epochtalk_server/models/banned_address.ex b/lib/epochtalk_server/models/banned_address.ex index 864fab55..8d108191 100644 --- a/lib/epochtalk_server/models/banned_address.ex +++ b/lib/epochtalk_server/models/banned_address.ex @@ -19,6 +19,8 @@ defmodule EpochtalkServer.Models.BannedAddress do field :updates, {:array, :naive_datetime} end + ## === Changesets Functions === + def changeset(banned_address, attrs \\ %{}) do now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) attrs = attrs @@ -36,12 +38,14 @@ defmodule EpochtalkServer.Models.BannedAddress do _ -> changeset_hostname(banned_address, attrs) end end + def changeset_hostname(banned_address, attrs \\ %{}) do banned_address |> cast(attrs, [:hostname, :weight, :decay, :imported_at, :created_at, :updates]) |> validate_required([:hostname, :weight, :decay, :created_at, :updates]) |> unique_constraint(:hostname, name: :banned_addresses_hostname_index) end + def changeset_ip(banned_address, attrs \\ %{}) do cs_data = banned_address |> cast(attrs, [:ip1, :ip2, :ip3, :ip4, :weight, :decay, :imported_at, :created_at, :updates]) @@ -60,9 +64,11 @@ defmodule EpochtalkServer.Models.BannedAddress do end end + ## === Database Functions === + def upsert(address_list) when is_list(address_list) do Repo.transaction(fn -> - Enum.each(address_list ,&BannedAddress.upsert(&1)) + Enum.each(address_list ,&upsert(&1)) end) end def upsert(banned_address) do @@ -104,6 +110,8 @@ defmodule EpochtalkServer.Models.BannedAddress do end end + ## === External Helper Functions === + def calculate_malicious_score_from_ip(ip) do case :inet.parse_address(to_charlist(ip)) do {:ok, ip} -> @@ -121,7 +129,10 @@ defmodule EpochtalkServer.Models.BannedAddress do end end + ## === Private Helper Functions === + defp hostname_from_host({ _, name, _, _, _, _ }), do: List.to_string(name) + defp calculate_hostname_score(hostname) do (from ba in BannedAddress, where: like(ba.hostname, ^hostname), @@ -129,6 +140,7 @@ defmodule EpochtalkServer.Models.BannedAddress do |> Repo.one |> calculate_score_decay end + defp calculate_ip32_score({ ip1, ip2, ip3, ip4 }) do (from ba in BannedAddress, where: ba.ip1 == ^ip1 and ba.ip2 == ^ip2 and ba.ip3 == ^ip3 and ba.ip4 == ^ip4, @@ -136,6 +148,7 @@ defmodule EpochtalkServer.Models.BannedAddress do |> Repo.one |> calculate_score_decay end + defp calculate_ip24_score({ ip1, ip2, ip3, _ }) do (from ba in BannedAddress, where: ba.ip1 == ^ip1 and ba.ip2 == ^ip2 and ba.ip3 == ^ip3, @@ -143,6 +156,7 @@ defmodule EpochtalkServer.Models.BannedAddress do |> Repo.one |> calculate_score_decay end + defp calculate_ip16_score({ ip1, ip2, _, _ }) do (from ba in BannedAddress, where: ba.ip1 == ^ip1 and ba.ip2 == ^ip2, @@ -166,15 +180,15 @@ defmodule EpochtalkServer.Models.BannedAddress do # returns the decayed score given a banned address # no banned address data found return 0 - def calculate_score_decay(), do: 0 - def calculate_score_decay(banned_address) when banned_address == %{}, do: 0 - def calculate_score_decay(banned_address) when is_nil(banned_address), do: 0 + defp calculate_score_decay(nil), do: 0 + defp calculate_score_decay(banned_address) when banned_address == %{}, do: 0 + defp calculate_score_decay(banned_address) when is_nil(banned_address), do: 0 # banned address data found and decays, calculate - def calculate_score_decay(%{ decay: true, updates: updates, weight: weight, created_at: created_at } = _banned_address) do + defp calculate_score_decay(%{ decay: true, updates: updates, weight: weight, created_at: created_at } = _banned_address) do last_update_date = if length(updates) > 0, do: List.last(updates), else: created_at diff_ms = abs(NaiveDateTime.diff(last_update_date, NaiveDateTime.utc_now()) * 1000) decay_for_time(diff_ms, weight) end # banned address data found, but does not decay - def calculate_score_decay(banned_address), do: Decimal.to_float(banned_address.weight) + defp calculate_score_decay(banned_address), do: Decimal.to_float(banned_address.weight) end diff --git a/lib/epochtalk_server/models/board.ex b/lib/epochtalk_server/models/board.ex index b0c6975a..5aa8a538 100644 --- a/lib/epochtalk_server/models/board.ex +++ b/lib/epochtalk_server/models/board.ex @@ -23,6 +23,8 @@ defmodule EpochtalkServer.Models.Board do many_to_many :categories, Category, join_through: BoardMapping end + ## === Changesets Functions === + def changeset(board, attrs) do board |> cast(attrs, [:id, :name, :slug, :description, :post_count, :thread_count, :viewable_by, :postable_by, :created_at, :imported_at, :updated_at, :meta]) @@ -40,7 +42,11 @@ defmodule EpochtalkServer.Models.Board do |> unique_constraint(:id, name: :boards_pkey) |> unique_constraint(:slug, name: :boards_slug_index) end + + ## === Database Functions === + def insert(%Board{} = board), do: Repo.insert(board) + def create(board) do board_cs = create_changeset(%Board{}, board) case Repo.insert(board_cs) do diff --git a/lib/epochtalk_server/models/board_mapping.ex b/lib/epochtalk_server/models/board_mapping.ex index 818030ad..01dadd50 100644 --- a/lib/epochtalk_server/models/board_mapping.ex +++ b/lib/epochtalk_server/models/board_mapping.ex @@ -15,6 +15,8 @@ defmodule EpochtalkServer.Models.BoardMapping do field :view_order, :integer end + ## === Changesets Functions === + def changeset(board_mapping, attrs) do board_mapping |> cast(attrs, [:board_id, :parent_id, :category_id, :view_order]) @@ -22,6 +24,9 @@ defmodule EpochtalkServer.Models.BoardMapping do |> unique_constraint([:board_id, :parent_id], name: :board_mapping_board_id_parent_id_index) |> unique_constraint([:board_id, :category_id], name: :board_mapping_board_id_category_id_index) end + + ## === Database Functions === + def insert(%BoardMapping{} = board_mapping), do: Repo.insert(board_mapping) def delete_board(id) do diff --git a/lib/epochtalk_server/models/board_moderator.ex b/lib/epochtalk_server/models/board_moderator.ex index cd0306df..5c3adce6 100644 --- a/lib/epochtalk_server/models/board_moderator.ex +++ b/lib/epochtalk_server/models/board_moderator.ex @@ -13,12 +13,16 @@ defmodule EpochtalkServer.Models.BoardModerator do belongs_to :board, Board end + ## === Changesets Functions === + def changeset(permission, attrs \\ %{}) do permission |> cast(attrs, [:user_id, :board_id]) |> validate_required([:user_id, :board_id]) end + ## === Database Functions === + defp get_boards_by_user_id(user_id) when is_integer(user_id) do from(bm in BoardModerator, where: bm.user_id == ^user_id) |> Repo.all() diff --git a/lib/epochtalk_server/models/category.ex b/lib/epochtalk_server/models/category.ex index cb6c1be6..f7f0ece5 100644 --- a/lib/epochtalk_server/models/category.ex +++ b/lib/epochtalk_server/models/category.ex @@ -18,12 +18,13 @@ defmodule EpochtalkServer.Models.Category do many_to_many :boards, Board, join_through: BoardMapping end + ## === Changesets Functions === + def changeset(category, attrs) do category |> cast(attrs, [:id, :name, :view_order, :viewable_by, :postable_by, :created_at, :imported_at, :updated_at, :meta]) |> unique_constraint(:id, name: :categories_pkey) end - def insert(%Category{} = category), do: Repo.insert(category) def create_changeset(category, attrs) do now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) @@ -33,25 +34,25 @@ defmodule EpochtalkServer.Models.Category do category |> cast(attrs, [:name, :viewable_by, :created_at, :updated_at]) end - def create(category) do - category_cs = create_changeset(%Category{}, category) - db_cat = Repo.insert!(category_cs) - %{ - id: db_cat.id, - name: db_cat.name, - viewable_by: db_cat.viewable_by - } - end - def changeset_for_board_mapping(category, attrs) do + def update_for_board_mapping_changeset(category, attrs) do category |> cast(attrs, [:id, :name, :view_order, :viewable_by]) |> unique_constraint(:id, name: :categories_pkey) end + ## === Database Functions === + + def insert(%Category{} = category), do: Repo.insert(category) + + def create(attrs) do + category_cs = create_changeset(%Category{}, attrs) + Repo.insert!(category_cs) + end + def update_for_board_mapping(%{ id: id } = category_map) do %Category{ id: id } - |> changeset_for_board_mapping(category_map) + |> update_for_board_mapping_changeset(category_map) |> Repo.update end end diff --git a/lib/epochtalk_server/models/invitation.ex b/lib/epochtalk_server/models/invitation.ex index 71cd2e98..bc4e252f 100644 --- a/lib/epochtalk_server/models/invitation.ex +++ b/lib/epochtalk_server/models/invitation.ex @@ -12,6 +12,8 @@ defmodule EpochtalkServer.Models.Invitation do field :created_at, :naive_datetime end + ## === Changesets Functions === + def create_changeset(invitation, attrs \\ %{}) do now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) attrs = attrs @@ -27,6 +29,9 @@ defmodule EpochtalkServer.Models.Invitation do ) end + ## === Database Functions === + def create(email), do: Repo.insert(create_changeset(%Invitation{}, %{email: email})) + def delete(email), do: Repo.delete_all(from(i in Invitation, where: i.email == ^email)) end diff --git a/lib/epochtalk_server/models/metadata_board.ex b/lib/epochtalk_server/models/metadata_board.ex index c1f1f5c8..ee469ee3 100644 --- a/lib/epochtalk_server/models/metadata_board.ex +++ b/lib/epochtalk_server/models/metadata_board.ex @@ -19,6 +19,8 @@ defmodule EpochtalkServer.Models.MetadataBoard do field :last_post_position, :integer end + ## === Changesets Functions === + def changeset(permission, attrs \\ %{}) do permission |> cast(attrs, [:id, :board_id, :post_count, :thread_count, @@ -27,5 +29,8 @@ defmodule EpochtalkServer.Models.MetadataBoard do :last_post_position]) |> validate_required([:board_id]) end + + ## === Database Functions === + def insert(%MetadataBoard{} = metadata_boards), do: Repo.insert(metadata_boards) end diff --git a/lib/epochtalk_server/models/permission.ex b/lib/epochtalk_server/models/permission.ex index c4267416..15cdef93 100644 --- a/lib/epochtalk_server/models/permission.ex +++ b/lib/epochtalk_server/models/permission.ex @@ -9,16 +9,17 @@ defmodule EpochtalkServer.Models.Permission do field :path, :string end + ## === Changesets Functions === + def changeset(permission, attrs \\ %{}) do permission |> cast(attrs, [:path]) |> validate_required([:path]) end - def by_path(path) - when is_binary(path) do - Repo.get_by(Permission, path: path) - end - def all() do - Repo.all(Permission) - end + + ## === Database Functions === + + def all(), do: Repo.all(Permission) + + def by_path(path) when is_binary(path), do: Repo.get_by(Permission, path: path) end diff --git a/lib/epochtalk_server/models/preference.ex b/lib/epochtalk_server/models/preference.ex index 1776e354..ff458377 100644 --- a/lib/epochtalk_server/models/preference.ex +++ b/lib/epochtalk_server/models/preference.ex @@ -19,6 +19,8 @@ defmodule EpochtalkServer.Models.Preference do field :email_messages, :boolean end + ## === Changesets Functions === + def changeset(permission, attrs \\ %{}) do permission |> cast(attrs, [:user_id, :posts_per_page, :threads_per_page, diff --git a/lib/epochtalk_server/models/profile.ex b/lib/epochtalk_server/models/profile.ex index 88b04044..4444f33d 100644 --- a/lib/epochtalk_server/models/profile.ex +++ b/lib/epochtalk_server/models/profile.ex @@ -15,6 +15,8 @@ defmodule EpochtalkServer.Models.Profile do field :last_active, :naive_datetime end + ## === Changesets Functions === + def changeset(permission, attrs \\ %{}) do permission |> cast(attrs, [:user_id, :avatar, :position, :signature, :raw_signature, :post_count, :field, :last_active]) diff --git a/lib/epochtalk_server/models/role.ex b/lib/epochtalk_server/models/role.ex index 42a98fb5..4b25d55c 100644 --- a/lib/epochtalk_server/models/role.ex +++ b/lib/epochtalk_server/models/role.ex @@ -3,6 +3,7 @@ defmodule EpochtalkServer.Models.Role do import Ecto.Changeset import Ecto.Query, only: [from: 2] alias EpochtalkServer.Repo + alias EpochtalkServer.Models.User alias EpochtalkServer.Models.Role alias EpochtalkServer.Models.RoleUser @@ -21,29 +22,31 @@ defmodule EpochtalkServer.Models.Role do field :updated_at, :naive_datetime end + ## === Changesets Functions === + def changeset(role, attrs \\ %{}) do role |> cast(attrs, [:name, :description, :lookup, :priority, :permissions]) |> validate_required([:name, :description, :lookup, :priority, :permissions]) end + + ## === Database Functions === + + ## SELECT OPERATIONS + def all, do: from(r in Role, order_by: r.id) |> Repo.all + + def get_banned_role_id(), do: Repo.one(from(r in Role, select: r.id, where: r.lookup == "banned")) + + def get_newbie_role_id(), do: Repo.one(from(r in Role, select: r.id, where: r.lookup == "newbie")) + + def get_default(), do: by_lookup("user") + def by_lookup(lookups) when is_list(lookups) do from(r in Role, where: r.lookup in ^lookups) |> Repo.all end def by_lookup(lookup), do: Repo.get_by(Role, lookup: lookup) - def insert([]), do: {:error, "Role list is empty"} - def insert(%Role{} = role), do: Repo.insert(role) - def insert([%{}|_] = roles), do: Repo.insert_all(Role, roles) - - def set_permissions(id, permissions) do - Role - |> Repo.get(id) - |> change(%{ permissions: permissions }) - |> Repo.update - end - def get_banned_role_id(), do: Repo.one(from(r in Role, select: r.id, where: r.lookup == "banned")) - def get_newbie_role_id(), do: Repo.one(from(r in Role, select: r.id, where: r.lookup == "newbie")) def by_user_id(user_id) do query = from ru in RoleUser, join: r in Role, @@ -54,17 +57,47 @@ defmodule EpochtalkServer.Models.Role do [] -> [get_default()] # user has no roles, return default role users_roles -> users_roles # user has roles, return them end - |> handle_banned_user_role # if banned, only [ banned ] is returned for roles + |> Role.handle_banned_user_role # if banned, only [ banned ] is returned for roles end - def get_default(), do: by_lookup("user") - defp handle_banned_user_role(roles), do: if ban_role = reduce_ban_role(roles), do: [ban_role], else: roles - defp reduce_ban_role([]), do: nil - defp reduce_ban_role([role | _]) when role.lookup === "banned", do: role - defp reduce_ban_role([_ | roles]), do: reduce_ban_role(roles) + ## CREATE OPERATIONS + + # For seeding roles + def insert([]), do: {:error, "Role list is empty"} + def insert(%Role{} = role), do: Repo.insert(role) + def insert([%{}|_] = roles), do: Repo.insert_all(Role, roles) + + ## UPDATE OPERATIONS + + def set_permissions(id, permissions) do + Role + |> Repo.get(id) + |> change(%{ permissions: permissions }) + |> Repo.update + end + + ## === External Helper Functions === + + # appends the default roles to User Model, if user has no roles + def handle_empty_user_roles(%User{roles: [%Role{} | _]} = user), do: user + def handle_empty_user_roles(%User{roles: []} = user), do: user |> Map.put(:roles, [Role.get_default()]) + def handle_empty_user_roles(%User{} = user), do: user |> Map.put(:roles, [Role.get_default()]) + + # called with user model, outputs user model with updated role + def handle_banned_user_role(%User{roles: [%Role{} | _] = roles} = user) do + if banned_role = Enum.find(roles, &(&1.lookup == "banned")), + do: user |> Map.put(:roles, [banned_role]), + else: user + end + # called with just roles, ouput updated roles + def handle_banned_user_role(roles), do: if ban_role = reduce_ban_role(roles), do: [ban_role], else: roles - def get_masked_permissions(roles) when is_list(roles), do: Enum.reduce(roles, %{}, &get_masked_permissions(&2, &1)) - defp get_masked_permissions(target, source) do + # Given %User{}.roles, outputs xored permissions + def get_masked_permissions(roles) when is_list(roles), do: Enum.reduce(roles, %{}, &mask_permissions(&2, &1)) + + ## === Private Helper Functions === + + defp mask_permissions(target, source) do merge_keys = [:highlight_color, :permissions, :priority, :priority_restrictions] filtered_source_keys = Map.keys(source) |> Enum.filter(&Enum.member?(merge_keys, &1)) target_is_lesser_role = !Map.get(target, :priority) or target.priority > source.priority @@ -92,4 +125,8 @@ defmodule EpochtalkServer.Models.Role do # NOT a map. We fall back to standard merge behavior, preferring # the value on the right. defp deep_resolve(_key, _left, right), do: right + + defp reduce_ban_role([]), do: nil + defp reduce_ban_role([role | _]) when role.lookup === "banned", do: role + defp reduce_ban_role([_ | roles]), do: reduce_ban_role(roles) end diff --git a/lib/epochtalk_server/models/role_permission.ex b/lib/epochtalk_server/models/role_permission.ex index 54d7d011..3f518174 100644 --- a/lib/epochtalk_server/models/role_permission.ex +++ b/lib/epochtalk_server/models/role_permission.ex @@ -18,11 +18,16 @@ defmodule EpochtalkServer.Models.RolePermission do field :modified, :boolean end + ## === Changesets Functions === + def changeset(role_permission, attrs \\ %{}) do role_permission |> cast(attrs, [:role_id, :permission_path, :value, :modified]) |> validate_required([:role_id, :permission_path, :value, :modified]) end + + ## === Database Functions === + def insert([]), do: {:error, "Role permission list is empty"} def insert(%RolePermission{} = role_permission), do: Repo.insert(role_permission) def insert([%{}|_] = roles_permissions), do: Repo.insert_all(RolePermission, roles_permissions) @@ -57,6 +62,7 @@ defmodule EpochtalkServer.Models.RolePermission do conflict_target: [:role_id, :permission_path] # check conflicts on unique index keys ) end + # derives a single nested map of all permissions for a role def permissions_map_by_role_id(role_id) do from(rp in RolePermission, @@ -68,6 +74,7 @@ defmodule EpochtalkServer.Models.RolePermission do |> Enum.reduce(%{}, fn %{permission_path: permission_path, value: value}, acc -> Map.put(acc, permission_path, value) end) |> Iteraptor.from_flatmap end + # for server-side role-loading use, only runs if roles permissions table is currently empty # sets all roles permissions to value: false, modified: false def maybe_init! do diff --git a/lib/epochtalk_server/models/role_user.ex b/lib/epochtalk_server/models/role_user.ex index abff6a43..e0891cfe 100644 --- a/lib/epochtalk_server/models/role_user.ex +++ b/lib/epochtalk_server/models/role_user.ex @@ -14,19 +14,25 @@ defmodule EpochtalkServer.Models.RoleUser do belongs_to :role, Role end + ## === Changesets Functions === + def changeset(role_user, attrs \\ %{}) do role_user |> cast(attrs, [:user_id, :role_id]) |> validate_required([:user_id, :role_id]) end + ## === Database Functions === + def set_admin(user_id), do: set_user_role(@admin_role_id, user_id) + def set_user_role(role_id, user_id) do case Repo.one(from(ru in RoleUser, where: ru.role_id == ^role_id and ru.user_id == ^user_id)) do nil -> Repo.insert(changeset(%RoleUser{}, %{role_id: role_id, user_id: user_id})) roleuser_cs -> {:ok, roleuser_cs} end end + def delete_user_role(role_id, user_id) do query = from ru in RoleUser, where: ru.role_id == ^role_id and ru.user_id == ^user_id diff --git a/lib/epochtalk_server/models/user.ex b/lib/epochtalk_server/models/user.ex index 9436a757..a748c123 100644 --- a/lib/epochtalk_server/models/user.ex +++ b/lib/epochtalk_server/models/user.ex @@ -32,10 +32,12 @@ defmodule EpochtalkServer.Models.User do has_one :preferences, Preference has_one :profile, Profile has_one :ban_info, Ban - has_many :roles, RoleUser + many_to_many :roles, Role, join_through: RoleUser has_many :moderating, BoardModerator end + ## === Changesets Functions === + def registration_changeset(user, attrs) do user |> cast(attrs, [:id, :email, :username, :created_at, :updated_at, :deleted, :malicious_score, :password]) @@ -44,29 +46,25 @@ defmodule EpochtalkServer.Models.User do |> validate_email() |> validate_password() end + + ## === Database Functions === + # create admin, for seeding def create(attrs, true = _admin) do Repo.transaction(fn -> - create(attrs) - |> case do {:ok, %{ id: id } = _user} -> RoleUser.set_admin(id) end + {:ok, user} = create(attrs) + RoleUser.set_admin(user.id) end) end def create(attrs) do - user_cs = User.registration_changeset(%User{}, attrs) - case Repo.insert(user_cs) do - {:ok, user} -> {:ok, user |> Map.put(:roles, Role.by_user_id(user.id))} # append roles - {:error, err} -> {:error, err} - end - end - def create_fixed(attrs) do user_cs = User.registration_changeset(%User{}, attrs) case Repo.insert(user_cs) do {:ok, user} -> user = user |> Repo.preload([:preferences, :profile, :ban_info, :moderating]) # load associations - |> Map.put(:roles, [%RoleUser{role: Role.get_default()}]) # default to user role - {:ok, user} # load associations - {:error, err} -> {:error, err} + |> Role.handle_empty_user_roles() # appends default role + {:ok, user} + {:error, err} -> {:error, err} # changeset error end end @@ -76,20 +74,17 @@ defmodule EpochtalkServer.Models.User do def by_id(id) when is_integer(id), do: Repo.get_by(User, id: id) - defp set_malicious_score(%User{ id: id } = user, malicious_score) do - set_malicious_score_by_id(id, malicious_score) - if malicious_score != nil, - do: user |> Map.put(:malicious_score, malicious_score), - else: user - end + def clear_malicious_score_by_id(id), do: set_malicious_score_by_id(id, nil) - defp set_malicious_score_by_id(id, malicious_score) do - from(u in User, where: u.id == ^id) - |> Repo.update_all(set: [malicious_score: malicious_score]) + def by_username(username) when is_binary(username) do + query = from u in User, + where: u.username == ^username, + preload: [:preferences, :profile, :ban_info, :moderating, :roles] + if user = Repo.one(query), + do: {:ok, user |> Role.handle_empty_user_roles() |> Role.handle_banned_user_role()}, + else: {:error, :user_not_found} end - def clear_malicious_score_by_id(id), do: set_malicious_score_by_id(id, nil) - def handle_malicious_user(%User{} = user, ip) do # convert ip tuple into string ip_str = ip |> :inet_parse.ntoa |> to_string @@ -103,91 +98,33 @@ defmodule EpochtalkServer.Models.User do else: Ban.ban(user) end - def by_username_fixed(username) when is_binary(username) do - query = from u in User, - where: u.username == ^username, - preload: [:preferences, :profile, :ban_info, :moderating, roles: [:role]] - user = Repo.one(query) - if user do - case length(user.roles) do - 0 -> {:ok, Map.put(user, :roles, [%RoleUser{role: Role.get_default()}])} - _ -> {:ok, user} - end - else - {:error, :user_not_found} - end - end - - def by_username(username) when is_binary(username) do - query = from u in User, - left_join: p in Profile, - on: u.id == p.user_id, - left_join: pr in Preference, - on: u.id == pr.user_id, - select: %{ - id: u.id, - username: u.username, - email: u.email, - passhash: u.passhash, - confirmation_token: u.confirmation_token, - reset_token: u.reset_token, - reset_expiration: u.reset_expiration, - deleted: u.deleted, - malicious_score: u.malicious_score, - created_at: u.created_at, - updated_at: u.updated_at, - imported_at: u.imported_at, - avatar: p.avatar, - position: p.position, - signature: p.signature, - raw_signature: p.raw_signature, - fields: p.fields, - post_count: p.post_count, - last_active: p.last_active, - posts_per_page: pr.posts_per_page, - threads_per_page: pr.threads_per_page, - collapsed_categories: pr.collapsed_categories, - ignored_boards: pr.ignored_boards, - ban_expiration: fragment(""" - CASE WHEN EXISTS ( - SELECT user_id - FROM roles_users - WHERE role_id = (SELECT id FROM roles WHERE lookup = \'banned\') and user_id = ? - ) - THEN ( - SELECT expiration - FROM users.bans - WHERE user_id = ? - ) - ELSE NULL END - """, u.id, u.id)}, - where: u.username == ^username - - case Repo.one(query) do - nil -> {:error, :user_not_found} - user -> - # set all user's roles - user = Map.put(user, :roles, Role.by_user_id(user.id)) - # set primary role info - primary_role = List.first(user[:roles]) - hc = Map.get(primary_role, :highlight_color) - user = user - |> Map.put(:role_name, primary_role.name) - |> Map.put(:role_highlight_color, (if hc, do: hc, else: "")) - |> format_user - {:ok, user} - end - end def valid_password?(%{passhash: hashed_password} = _user, password) when is_binary(hashed_password) and byte_size(password) > 0 do Argon2.verify_pass(password, hashed_password) end + + ## === Private Helper Functions === + # sets malicious score of a user, outputs user with updated malicious score + defp set_malicious_score(%User{ id: id } = user, malicious_score) do + set_malicious_score_by_id(id, malicious_score) + if malicious_score != nil, + do: user |> Map.put(:malicious_score, malicious_score), + else: user + end + + # sets malicious score of a user + defp set_malicious_score_by_id(id, malicious_score) do + from(u in User, where: u.id == ^id) + |> Repo.update_all(set: [malicious_score: malicious_score]) + end + defp validate_username(changeset) do changeset |> validate_required(:username) |> validate_length(:username, min: 3, max: 255) |> unique_constraint(:username) end + defp validate_email(changeset) do changeset |> validate_required([:email]) @@ -196,6 +133,7 @@ defmodule EpochtalkServer.Models.User do |> unsafe_validate_unique(:email, Repo) |> unique_constraint(:email) end + defp validate_password(changeset) do changeset |> validate_required([:password]) @@ -209,9 +147,9 @@ defmodule EpochtalkServer.Models.User do # |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character") |> hash_password() end + defp hash_password(changeset) do password = get_change(changeset, :password) - if password && changeset.valid? do changeset |> put_change(:passhash, Argon2.hash_pwd_salt(password)) @@ -220,12 +158,4 @@ defmodule EpochtalkServer.Models.User do changeset end end - defp format_user(user) do - user = Map.filter(user, fn {_, v} -> v end) # remove nil - user = if f = Map.get(user, :fields), do: Map.merge(user, f), else: user # merge fields onto user - user = if cc = Map.get(user, :collapsed_categories), do: Map.put(user, :collapsed_categories, Map.get(cc, "cats")), else: user # unnest cats - user = if ib = Map.get(user, :ignored_boards), do: Map.put(user, :ignored_boards, Map.get(ib, "boards")), else: user #unnest boards - Map.delete(user, :fields) # strip fields from user - |> Map.put(:avatar, Map.get(user, :avatar)) # sets avatar back to nil if not set - end end diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index abb09af8..fee86cfb 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -38,7 +38,7 @@ defmodule EpochtalkServer.Session do # use default role def update_roles(user_id, roles) when is_list(roles) do # save/replace roles to redis under "user:{user_id}:roles" - role_lookups = roles |> Enum.map(&(&1.role.lookup)) + role_lookups = roles |> Enum.map(&(&1.lookup)) role_key = generate_key(user_id, "roles") Redix.command(:redix, ["DEL", role_key]) unless role_lookups == nil or role_lookups == [], do: diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/auth_controller.ex index ebabe44a..8abfb92d 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/auth_controller.ex @@ -3,7 +3,6 @@ defmodule EpochtalkServerWeb.AuthController do alias EpochtalkServer.Models.User alias EpochtalkServer.Models.Ban alias EpochtalkServer.Models.Invitation - # alias EpochtalkServer.Models.BoardModerator alias EpochtalkServer.Auth.Guardian alias EpochtalkServer.Session alias EpochtalkServerWeb.ErrorHelpers @@ -16,21 +15,22 @@ defmodule EpochtalkServerWeb.AuthController do } def username(conn, %{"username" => username}), do: render(conn, "search.json", found: User.with_username_exists?(username)) + def email(conn, %{"email" => email}), do: render(conn, "search.json", found: User.with_email_exists?(email)) # TODO(akinsey): handle config.inviteOnly # TODO(akinsey): handle config.newbieEnabled # TODO(akinsey): Send confirmation email def register(conn, %{"username" => _, "email" => _, "password" => _} = attrs) do - with {:auth, false} <- {:auth, Guardian.Plug.authenticated?(conn)}, - {:ok, user} <- User.create_fixed(attrs), - {_count, nil} <- Invitation.delete(user.email), - {:ok, user} <- User.handle_malicious_user(user, conn.remote_ip), - {:ok, user, token, conn} <- Session.create(user, false, conn) do + with {:auth, false} <- {:auth, Guardian.Plug.authenticated?(conn)}, # check auth + {:ok, user} <- User.create(attrs), # create user + {_count, nil} <- Invitation.delete(user.email), # delete invitation + {:ok, user} <- User.handle_malicious_user(user, conn.remote_ip), # ban if malicious + {:ok, user, token, conn} <- Session.create(user, false, conn) do # create session render(conn, "user.json", %{ user: user, token: token}) else # user already authenticated - {:auth, true} -> authenticate(conn) + {:auth, true} -> ErrorHelpers.render_json_error(conn, 400, "Cannot register a new account while logged in") # error banning in handle_malicious_user {:error, :ban_error} -> ErrorHelpers.render_json_error(conn, 500, "There was an error banning malicious user, upon login") # error in user.create @@ -60,37 +60,17 @@ defmodule EpochtalkServerWeb.AuthController do end def login(conn, attrs) when not is_map_key(attrs, "rememberMe"), do: login(conn, Map.put(attrs, "rememberMe", false)) - # def login(conn, %{"username" => username, "password" => password, "rememberMe" => _} = _attrs) do - # with {:auth, false} <- {:auth, Guardian.Plug.authenticated?(conn)}, - # {:ok, user} <- User.by_username(username), - # {:not_confirmed, false} <- {:not_confirmed, !!Map.get(user, :confirmation_token)}, - # {:missing_passhash, true} <- {:missing_passhash, !!Map.get(user, :passhash)}, - # {:valid_password, true} <- {:valid_password, User.valid_password?(user, password)}, - # {:ok, user} <- Ban.unban(user), - # {:ok, user} <- BoardModerator.get_boards(user), - # {user, conn} <- Session.create(user, conn) do - # render(conn, "user.json", user: user) - # else - # {:auth, true} -> authenticate(conn) - # {:error, :user_not_found} -> raise(InvalidCredentials) - # {:not_confirmed, true} -> raise(AccountNotConfirmed) - # {:missing_passhash, false} -> raise(AccountMigrationNotComplete) - # {:valid_password, false} -> raise(InvalidCredentials) - # {:error, :unban_error} -> ErrorHelpers.render_json_error(conn, 500, "There was an issue unbanning user, upon login") - # _ -> ErrorHelpers.render_json_error(conn, 500, "There was an issue signing out") - # end - # end def login(conn, %{"username" => username, "password" => password, "rememberMe" => remember_me} = _attrs) do with {:auth, false} <- {:auth, Guardian.Plug.authenticated?(conn)}, - {:ok, user} <- User.by_username_fixed(username), + {:ok, user} <- User.by_username(username), {:not_confirmed, false} <- {:not_confirmed, !!Map.get(user, :confirmation_token)}, {:missing_passhash, true} <- {:missing_passhash, !!Map.get(user, :passhash)}, {:valid_password, true} <- {:valid_password, User.valid_password?(user, password)}, - {:ok, user} <- Ban.unban_fixed(user), - {:ok, user, token, conn} <- Session.create(user, remember_me, conn) do + {:ok, user} = Ban.unban(user), + {:ok, user, token, conn} = Session.create(user, remember_me, conn) do render(conn, "user.json", %{ user: user, token: token}) else - {:auth, true} -> authenticate(conn) + {:auth, true} -> ErrorHelpers.render_json_error(conn, 400, "Already logged in") {:error, :user_not_found} -> raise(InvalidCredentials) {:not_confirmed, true} -> raise(AccountNotConfirmed) {:missing_passhash, false} -> raise(AccountMigrationNotComplete) diff --git a/lib/epochtalk_server_web/views/custom_errors.ex b/lib/epochtalk_server_web/errors/custom_errors.ex similarity index 99% rename from lib/epochtalk_server_web/views/custom_errors.ex rename to lib/epochtalk_server_web/errors/custom_errors.ex index 2c226cde..b62cfa49 100644 --- a/lib/epochtalk_server_web/views/custom_errors.ex +++ b/lib/epochtalk_server_web/errors/custom_errors.ex @@ -7,18 +7,21 @@ defmodule EpochtalkServerWeb.CustomErrors do """ defexception plug_status: 400, message: "Invalid credentials", conn: nil, router: nil end + defmodule NotLoggedIn do @moduledoc """ Exception raised when user is not logged in """ defexception plug_status: 400, message: "Not logged in", conn: nil, router: nil end + defmodule AccountNotConfirmed do @moduledoc """ Exception raised when user's account is not confirmed """ defexception plug_status: 400, message: "User account not confirmed", conn: nil, router: nil end + defmodule AccountMigrationNotComplete do @moduledoc """ Exception raised when user's account is not fully migrated diff --git a/lib/epochtalk_server_web/views/error_helpers.ex b/lib/epochtalk_server_web/errors/error_helpers.ex similarity index 98% rename from lib/epochtalk_server_web/views/error_helpers.ex rename to lib/epochtalk_server_web/errors/error_helpers.ex index a054b414..35ff42f4 100644 --- a/lib/epochtalk_server_web/views/error_helpers.ex +++ b/lib/epochtalk_server_web/errors/error_helpers.ex @@ -8,8 +8,9 @@ defmodule EpochtalkServerWeb.ErrorHelpers do @doc """ Renders error json from error data which could be a message or changeset errors. """ - def render_json_error(conn, status, %Ecto.Changeset{} = changeset), do: + def render_json_error(conn, status, %Ecto.Changeset{} = changeset) do render_json_error(conn, status, changeset_error_to_string(changeset)) + end def render_json_error(conn, status, message) do conn |> put_status(status) diff --git a/lib/epochtalk_server_web/views/guardian_error_handler.ex b/lib/epochtalk_server_web/errors/guardian_error_handler.ex similarity index 100% rename from lib/epochtalk_server_web/views/guardian_error_handler.ex rename to lib/epochtalk_server_web/errors/guardian_error_handler.ex diff --git a/lib/epochtalk_server_web/plugs/prepare_parse.ex b/lib/epochtalk_server_web/plugs/prepare_parse.ex index 015dfb70..ab2091be 100644 --- a/lib/epochtalk_server_web/plugs/prepare_parse.ex +++ b/lib/epochtalk_server_web/plugs/prepare_parse.ex @@ -2,7 +2,7 @@ defmodule EpochtalkServerWeb.Plugs.PrepareParse do import Plug.Conn use Phoenix.Controller alias EpochtalkServerWeb.ErrorView - @env Application.get_env(:application_api, :env) + @env Application.compile_env(:application_api, :env) @methods ~w(POST PUT PATCH) def init(opts), do: opts @@ -12,33 +12,17 @@ defmodule EpochtalkServerWeb.Plugs.PrepareParse do if method in @methods and not (@env in [:test]) do case Plug.Conn.read_body(conn, opts) do - {:error, :timeout} -> - raise Plug.TimeoutError - - {:error, _} -> - raise Plug.BadRequestError - - {:more, _, conn} -> - # raise Plug.PayloadTooLargeError, conn: conn, router: __MODULE__ - error = %{message: "Payload too large error"} - render_error(conn, error) - - {:ok, "" = body, conn} -> - update_in(conn.assigns[:raw_body], &[body | &1 || []]) - + {:error, :timeout} -> raise Plug.TimeoutError + {:error, _} -> raise Plug.BadRequestError + {:more, _, conn} -> render_error(conn, %{message: "Payload too large error"}) + {:ok, "" = body, conn} -> update_in(conn.assigns[:raw_body], &[body | &1 || []]) {:ok, body, conn} -> case Jason.decode(body) do - {:ok, _result} -> - update_in(conn.assigns[:raw_body], &[body | &1 || []]) - - {:error, _reason} -> - error = %{message: "Malformed JSON in the body"} - render_error(conn, error) + {:ok, _result} -> update_in(conn.assigns[:raw_body], &[body | &1 || []]) + {:error, _reason} -> render_error(conn, %{message: "Malformed JSON in the body"}) end end - else - conn - end + else conn end end def render_error(conn, %{ message: message } = _error) do diff --git a/lib/epochtalk_server_web/views/auth_view.ex b/lib/epochtalk_server_web/views/auth_view.ex index 397f568f..c48740d4 100644 --- a/lib/epochtalk_server_web/views/auth_view.ex +++ b/lib/epochtalk_server_web/views/auth_view.ex @@ -9,34 +9,30 @@ defmodule EpochtalkServerWeb.AuthView do def render("user.json", %{user: user, token: token}), do: format_user_reply(user, token) - - # Format reply from User ecto schema + # Format reply - from Models.User (login, register) defp format_user_reply(%User{} = user, token) do - avatar = if !!user.profile, do: user.profile.avatar, else: nil - permissions = Role.get_masked_permissions(Enum.map(user.roles, &(&1.role))) - roles = Enum.map(user.roles, &(&1.role.lookup)) + avatar = if user.profile, do: user.profile.avatar, else: nil moderating = if length(user.moderating) != 0, do: Enum.map(user.moderating, &(&1.board_id)), else: nil - ban_expiration = if !!user.ban_info, do: user.ban_info.expiration, else: nil - malicious_score = if !!user.malicious_score, do: user.malicious_score, else: nil - format_user_reply(user, token, {avatar, permissions, roles, moderating, ban_expiration, malicious_score}) + ban_expiration = if user.ban_info, do: user.ban_info.expiration, else: nil + malicious_score = if user.malicious_score, do: user.malicious_score, else: nil + format_user_reply(user, token, {avatar, moderating, ban_expiration, malicious_score}) end + # Format reply - from user stored by Guardian (authenticate) defp format_user_reply(user, token) do avatar = Map.get(user, :avatar) - permissions = Role.get_masked_permissions(user.roles) - roles = Enum.map(user.roles, &(&1.lookup)) moderating = if Map.get(user, :moderating) && length(user.moderating) != 0, do: user.moderating, else: nil ban_expiration = Map.get(user, :ban_expiration) malicious_score = Map.get(user, :malicious_score) - format_user_reply(user, token, {avatar, permissions, roles, moderating, ban_expiration, malicious_score}) + format_user_reply(user, token, {avatar, moderating, ban_expiration, malicious_score}) end - - defp format_user_reply(user, token, {avatar, permissions, roles, moderating, ban_expiration, malicious_score}) do + # Format reply - common user formatting functionality, outputs user reply map + defp format_user_reply(user, token, {avatar, moderating, ban_expiration, malicious_score}) do reply = %{ token: token, id: user.id, username: user.username, - permissions: permissions, - roles: roles + permissions: Role.get_masked_permissions(user.roles), + roles: Enum.map(user.roles, &(&1.lookup)) } # only append avatar, moderating, ban_expiration and malicious_score if present reply = if avatar, do: Map.put(reply, :avatar, avatar), else: reply @@ -45,27 +41,4 @@ defmodule EpochtalkServerWeb.AuthView do reply = if malicious_score, do: Map.put(reply, :malicious_score, malicious_score), else: reply reply end - - # defp format_user_reply(user, token) do - # reply = %{ - # token: token, - # id: user.id, - # username: user.username, - # avatar: Map.get(user, :avatar), # user.avatar - # permissions: Role.get_masked_permissions(user.roles), # user.permissions - # roles: Enum.map(user.roles, &(&1.lookup)) - # } - # # only append moderating, ban_expiration and malicious_score if present - # moderating = Map.get(user, :moderating) - # reply = if !is_nil(moderating) && length(moderating) != 0, - # do: Map.put(reply, :moderating, moderating), - # else: reply - # reply = if exp = Map.get(user, :ban_expiration), - # do: Map.put(reply, :ban_expiration, exp), - # else: reply - # reply = if ms = Map.get(user, :malicious_score), - # do: Map.put(reply, :malicious_score, ms), - # else: reply - # reply - # end end diff --git a/lib/epochtalk_server_web/views/error_view.ex b/lib/epochtalk_server_web/views/error_view.ex index eb773ae3..95816076 100644 --- a/lib/epochtalk_server_web/views/error_view.ex +++ b/lib/epochtalk_server_web/views/error_view.ex @@ -15,10 +15,7 @@ defmodule EpochtalkServerWeb.ErrorView do # match assigns.message and assigns.status defp format_error(%{message: message, status: status}), do: format_error(status, message) # match stack and status (Ex: function error from redis in session) - defp format_error(%{stack: _, status: status}) do - format_error(status, "Something went wrong") - end - + defp format_error(%{stack: _, status: status}), do: format_error(status, "Something went wrong") # format errors with readable message defp format_error(status, message) when is_binary(message) do %{ @@ -28,5 +25,5 @@ defmodule EpochtalkServerWeb.ErrorView do } end # format errors with unknown error - defp format_error(status, data), do: format_error(status, "Something went wrong") + defp format_error(status, _data), do: format_error(status, "Something went wrong") end From b701ef8743f38dd10b09672601f90f6647b0be0b Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 3 Oct 2022 12:06:41 -1000 Subject: [PATCH 175/231] docs(ban): Add rough draft of module docs to Ban model --- lib/epochtalk_server/models/ban.ex | 56 +++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/lib/epochtalk_server/models/ban.ex b/lib/epochtalk_server/models/ban.ex index 41d7adb0..c7199165 100644 --- a/lib/epochtalk_server/models/ban.ex +++ b/lib/epochtalk_server/models/ban.ex @@ -6,6 +6,9 @@ defmodule EpochtalkServer.Models.Ban do alias EpochtalkServer.Models.Role alias EpochtalkServer.Models.RoleUser alias EpochtalkServer.Models.Ban + @moduledoc """ + Ban model, for performing actions relating to banning + """ # max naivedatetime, used for permanent bans @max_date ~N[9999-12-31 00:00:00.000] @@ -21,13 +24,22 @@ defmodule EpochtalkServer.Models.Ban do ## === Changesets Functions === + @doc """ + Create generic changeset for `Ban` model + + Returns `%EpochtalkServer.Models.Ban{}` + """ def changeset(ban, attrs \\ %{}) do ban |> cast(attrs, [:id, :user_id, :expiration, :created_at, :updated_at]) |> validate_required([:user_id]) end - # handles upsert of ban for banning + @doc """ + Create ban changeset for `Ban` model, handles upsert of ban for banning + + Returns `%EpochtalkServer.Models.Ban{}` + """ def ban_changeset(ban, attrs \\ %{}) do now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) attrs = attrs @@ -39,7 +51,11 @@ defmodule EpochtalkServer.Models.Ban do |> validate_required([:user_id]) end - # handles update of ban for unbanning + @doc """ + Create unban changeset for `Ban` model, handles update of ban for unbanning + + Returns `%EpochtalkServer.Models.Ban{}` + """ def unban_changeset(ban, attrs \\ %{}) do now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) attrs = attrs @@ -52,8 +68,19 @@ defmodule EpochtalkServer.Models.Ban do ## === Database Functions === + @doc """ + Fetches `Ban` associated for a specific `User` + + Returns `%EpochtalkServer.Models.Ban{} | nil` + """ def by_user_id(user_id) when is_integer(user_id), do: Repo.get_by(Ban, user_id: user_id) + @doc """ + Used to ban a `User` by `user_id` until supplied `expiration`. Passing `nil` for `expiration` will + permanently ban the `User` + + Returns `{:ok, %EpochtalkServer.Models.Ban{}} | {:error, :ban_error}` + """ def ban_by_user_id(user_id, expiration) do Repo.transaction(fn -> RoleUser.set_user_role(Role.get_banned_role_id, user_id) @@ -65,9 +92,19 @@ defmodule EpochtalkServer.Models.Ban do end) end - # Ban without expiration, is a permanent ban, expiration nil + @doc """ + Used to ban a `User` permanently. Updates supplied `User` model to reflect ban and returns. + + Returns `{:ok, %EpochtalkServer.Models.User{}}` + """ def ban(%User{} = user), do: ban(user, nil) - # Ban has an expiration + + @doc """ + Used to ban a `User` until supplied `expiration`. Passing `nil` for `expiration` will + permanently ban the `User`. Updates supplied `User` model to reflect ban and returns. + + Returns `{:ok, %EpochtalkServer.Models.User{}} | {:error, :ban_error}` + """ def ban(%User{ id: id} = user, expiration) do case ban_by_user_id(id, expiration) do {:ok, _ban_info} -> # successful ban, update roles/ban info on user @@ -81,6 +118,11 @@ defmodule EpochtalkServer.Models.Ban do end end + @doc """ + Used to unban a `User` by `user_id`. + + Returns `{:ok, %EpochtalkServer.Models.Ban{}} | {:ok, nil} | {:error, :unban_error}` + """ def unban_by_user_id(user_id) when is_integer(user_id) do Repo.transaction(fn -> RoleUser.delete_user_role(Role.get_banned_role_id, user_id) # delete ban role from user @@ -92,7 +134,11 @@ defmodule EpochtalkServer.Models.Ban do end) end - # unban if ban is expired, return user's roles + @doc """ + Used to unban a `User`. Updates supplied `User` model to reflect unbanning and returns. + + Returns `{:ok, %EpochtalkServer.Models.User{}} | {:error, :unban_error}` + """ def unban(%User{ban_info: %Ban{expiration: expiration}, id: user_id} = user) do if NaiveDateTime.compare(expiration, NaiveDateTime.utc_now) == :lt do case unban_by_user_id(user_id) do From 2efbb32e4c46816d19384823afd1ea36a2cae9d7 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 3 Oct 2022 12:08:06 -1000 Subject: [PATCH 176/231] docs(role): add rough docs to role helper methods --- lib/epochtalk_server/models/role.ex | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/epochtalk_server/models/role.ex b/lib/epochtalk_server/models/role.ex index 4b25d55c..62d84430 100644 --- a/lib/epochtalk_server/models/role.ex +++ b/lib/epochtalk_server/models/role.ex @@ -78,11 +78,22 @@ defmodule EpochtalkServer.Models.Role do ## === External Helper Functions === - # appends the default roles to User Model, if user has no roles + + @doc """ + default role is not stored in the database, in order to save space + checks the role array on the user model + if roles array is empty, sets the default role by appending it + + """ def handle_empty_user_roles(%User{roles: [%Role{} | _]} = user), do: user def handle_empty_user_roles(%User{roles: []} = user), do: user |> Map.put(:roles, [Role.get_default()]) def handle_empty_user_roles(%User{} = user), do: user |> Map.put(:roles, [Role.get_default()]) + @doc """ + The `banned` `Role` takes priority over all other roles + If a `User` is banned, only return the `banned` `Role` + + """ # called with user model, outputs user model with updated role def handle_banned_user_role(%User{roles: [%Role{} | _] = roles} = user) do if banned_role = Enum.find(roles, &(&1.lookup == "banned")), From 8b3df773253596421a3d61b2e5f35fe35f91b202 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 3 Oct 2022 12:10:11 -1000 Subject: [PATCH 177/231] feat(mix): add ex_doc dependency for generating documentation from module docs --- mix.exs | 1 + mix.lock | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/mix.exs b/mix.exs index 275ebcc6..850b9340 100644 --- a/mix.exs +++ b/mix.exs @@ -36,6 +36,7 @@ defmodule EpochtalkServer.MixProject do {:argon2_elixir, "~> 3.0.0"}, {:corsica, "~> 1.2.0"}, {:ecto_sql, "~> 3.6"}, + {:ex_doc, "~> 0.28.5"}, {:guardian, "~> 2.2.4"}, {:guardian_db, "~> 2.1.0"}, {:guardian_redis, "~> 0.1.0"}, diff --git a/mix.lock b/mix.lock index 1484450a..9ce9cd58 100644 --- a/mix.lock +++ b/mix.lock @@ -9,16 +9,22 @@ "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, "db_connection": {:hex, :db_connection, "2.4.2", "f92e79aff2375299a16bcb069a14ee8615c3414863a6fef93156aee8e86c2ff3", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4fe53ca91b99f55ea249693a0229356a08f4d1a7931d8ffa79289b145fe83668"}, "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.27", "755da957e2b980618ba3397d3f923004d85bac244818cf92544eaa38585cb3a8", [:mix], [], "hexpm", "8d02465c243ee96bdd655e7c9a91817a2a80223d63743545b2861023c4ff39ac"}, "ecto": {:hex, :ecto, "3.8.4", "e06b8b87e62b27fea17fd2ff6041572ddd10339fd16cdf58446e402c6c90a74b", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f9244288b8d42db40515463a008cf3f4e0e564bb9c249fe87bf28a6d79fe82d4"}, "ecto_sql": {:hex, :ecto_sql, "3.8.3", "a7d22c624202546a39d615ed7a6b784580391e65723f2d24f65941b4dd73d471", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.8.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "348cb17fb9e6daf6f251a87049eafcb57805e2892e5e6a0f5dea0985d367329b"}, "elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"}, + "ex_doc": {:hex, :ex_doc, "0.28.5", "3e52a6d2130ce74d096859e477b97080c156d0926701c13870a4e1f752363279", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "d2c4b07133113e9aa3e9ba27efb9088ba900e9e51caa383919676afdf09ab181"}, "guardian": {:hex, :guardian, "2.2.4", "3dafdc19665411c96b2796d184064d691bc08813a132da5119e39302a252b755", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "6f83d4309c16ec2469da8606bb2a9815512cc2fac1595ad34b79940a224eb110"}, "guardian_db": {:hex, :guardian_db, "2.1.0", "ec95a9d99cdd1e550555d09a7bb4a340d8887aad0697f594590c2fd74be02426", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:guardian, "~> 1.0 or ~> 2.0", [hex: :guardian, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "f8e7d543ac92c395f3a7fd5acbe6829faeade57d688f7562e2f0fca8f94a0d70"}, "guardian_redis": {:hex, :guardian_redis, "0.1.0", "705f8291346d813755a701e0241acaa845453d662aca675f1de8f0821554a6f9", [:mix], [{:guardian_db, "~> 2.0", [hex: :guardian_db, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:redix, "~> 1.0", [hex: :redix, repo: "hexpm", optional: false]}], "hexpm", "8058f6613f888a2f10836f6b669be3f21c7d303955862b638fc3c1bafaf66bef"}, "iteraptor": {:git, "https://github.com/epochtalk/elixir-iteraptor.git", "d8d1c386c38e06bdfcf60c9ce1abf8e49161cab4", [tag: "1.13.1"]}, "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, "jose": {:hex, :jose, "1.11.2", "f4c018ccf4fdce22c71e44d471f15f723cb3efab5d909ab2ba202b5bf35557b3", [:mix, :rebar3], [], "hexpm", "98143fbc48d55f3a18daba82d34fe48959d44538e9697c08f34200fa5f0947d2"}, + "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, "phoenix": {:hex, :phoenix, "1.6.11", "29f3c0fd12fa1fc4d4b05e341578e55bc78d96ea83a022587a7e276884d397e4", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1664e34f80c25ea4918fbadd957f491225ef601c0e00b4e644b1a772864bfbc2"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"}, From 4ca95811c2249280be728ec4e7b0ef86f95c6f68 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 3 Oct 2022 12:11:55 -1000 Subject: [PATCH 178/231] refactor(cachebodyreader): update CacheBodyReader to be inside EpochtalkServerWeb.Endpoint --- lib/epochtalk_server_web/endpoint.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/epochtalk_server_web/endpoint.ex b/lib/epochtalk_server_web/endpoint.ex index cb4431a1..1ab695a3 100644 --- a/lib/epochtalk_server_web/endpoint.ex +++ b/lib/epochtalk_server_web/endpoint.ex @@ -1,5 +1,7 @@ defmodule EpochtalkServerWeb.Endpoint do use Phoenix.Endpoint, otp_app: :epochtalk_server + alias EpochtalkServerWeb.Endpoint.CacheBodyReader + # get x-forwarded ip plug RemoteIp @@ -40,4 +42,4 @@ defmodule EpochtalkServerWeb.Endpoint do end # used to help preparse raw req body, in case of malformed payload -defmodule CacheBodyReader, do: def read_body(conn, _opts), do: {:ok, conn.assigns.raw_body, conn} +defmodule EpochtalkServerWeb.Endpoint.CacheBodyReader, do: def read_body(conn, _opts), do: {:ok, conn.assigns.raw_body, conn} From 1e41d65d290f6fd6d525713d508aa0bc0ce4f956 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 3 Oct 2022 12:13:20 -1000 Subject: [PATCH 179/231] refactor(board-moderator): remove unused code, now that we are doing things the Ecto way. --- lib/epochtalk_server/models/board_moderator.ex | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/lib/epochtalk_server/models/board_moderator.ex b/lib/epochtalk_server/models/board_moderator.ex index 5c3adce6..48f82520 100644 --- a/lib/epochtalk_server/models/board_moderator.ex +++ b/lib/epochtalk_server/models/board_moderator.ex @@ -20,19 +20,4 @@ defmodule EpochtalkServer.Models.BoardModerator do |> cast(attrs, [:user_id, :board_id]) |> validate_required([:user_id, :board_id]) end - - ## === Database Functions === - - defp get_boards_by_user_id(user_id) when is_integer(user_id) do - from(bm in BoardModerator, where: bm.user_id == ^user_id) - |> Repo.all() - |> Enum.map(&(&1.board_id)) - end - - def get_boards(%{ id: user_id } = user) do - moderating = get_boards_by_user_id(user_id) - if length(moderating) > 1, - do: {:ok, Map.put(user, :moderating, moderating)}, - else: {:ok, user} - end end From 320e57ae74c19f80285f20feefd41a04f2d89fa3 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 3 Oct 2022 12:15:41 -1000 Subject: [PATCH 180/231] refactor(ban-error-handling): swallow error, return atom returning atoms relating to the database function called this allows us to bubble up the error and handle it properly using a "with" statement in our api controllers --- lib/epochtalk_server/models/ban.ex | 32 ++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/lib/epochtalk_server/models/ban.ex b/lib/epochtalk_server/models/ban.ex index c7199165..4c60a28f 100644 --- a/lib/epochtalk_server/models/ban.ex +++ b/lib/epochtalk_server/models/ban.ex @@ -84,12 +84,19 @@ defmodule EpochtalkServer.Models.Ban do def ban_by_user_id(user_id, expiration) do Repo.transaction(fn -> RoleUser.set_user_role(Role.get_banned_role_id, user_id) - case Repo.get_by(Ban, user_id: user_id) do - nil -> ban_changeset(%Ban{}, %{user_id: user_id, expiration: expiration}) - ban -> ban_changeset(ban, %{user_id: user_id, expiration: expiration}) + case Repo.get_by(Ban, user_id: user_id) do # look for existing ban + nil -> ban_changeset(%Ban{}, %{user_id: user_id, expiration: expiration}) # create new + ban -> ban_changeset(ban, %{user_id: user_id, expiration: expiration}) # update existing end |> Repo.insert_or_update! end) + |> case do + {:ok, ban_changeset} -> {:ok, ban_changeset} + {:error, err} -> # print error, return error atom + # TODO(akinsey): handle in logger (telemetry possibly) + IO.inspect err + {:error, :ban_error} + end end @doc """ @@ -112,9 +119,7 @@ defmodule EpochtalkServer.Models.Ban do |> Repo.preload([:ban_info, :roles], force: true) |> Role.handle_banned_user_role() # only return banned role inside roles once user is banned {:ok, user} - {:error, err} -> # print error, return error atom - IO.inspect err - {:error, :ban_error} + {:error, _} -> {:error, :ban_error} end end @@ -132,6 +137,12 @@ defmodule EpochtalkServer.Models.Ban do cs -> Repo.update!(unban_changeset(cs, %{ user_id: user_id })) end # unban the user end) + |> case do + {:ok, ban_changeset} -> {:ok, ban_changeset} + {:error, err} -> # print error, return error atom + IO.inspect err + {:error, :unban_error} + end end @doc """ @@ -142,16 +153,15 @@ defmodule EpochtalkServer.Models.Ban do def unban(%User{ban_info: %Ban{expiration: expiration}, id: user_id} = user) do if NaiveDateTime.compare(expiration, NaiveDateTime.utc_now) == :lt do case unban_by_user_id(user_id) do - {:ok, _result} -> #successful unban, update user roles and ban_info + {:ok, nil} -> {:ok, user} # user wasn't banned, return user + {:ok, _result} -> # successful unban, update user roles and ban_info user = user |> Repo.preload([:roles], force: true) - |> Role.handle_empty_user_roles() # check if roles are empty after unbanning + |> Role.handle_empty_user_roles() # if user's roles empty, default to user role |> Map.put(:malicious_score, nil) # clear malicious score |> Map.put(:ban_info, nil) # clear ban info so session gets updated {:ok, user} - {:error, err} -> # print error, return error atom - IO.inspect err - {:error, :unban_error} + {:error, _} -> {:error, :unban_error} end else {:ok, user} From b50d18c8be14418c202ecf0d93ec06b58a310ce1 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 3 Oct 2022 12:17:47 -1000 Subject: [PATCH 181/231] refactor(session): add User schema pattern match for session save --- lib/epochtalk_server/session.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index fee86cfb..74f70e0f 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -24,7 +24,7 @@ defmodule EpochtalkServer.Session do # return user with token {:ok, user, encoded_token, conn} end - defp save(user, session_id) do + defp save(%User{} = user, session_id) do # TODO: return role lookups from db instead of entire roles avatar = if is_nil(user.profile), do: nil, else: user.profile.avatar update_user_info(user.id, user.username, avatar) From 507433f11594368940efecd6035c5fa62c010ccc Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 6 Oct 2022 10:43:34 -1000 Subject: [PATCH 182/231] refactor(board-moderator): Remove unused imports and aliases --- lib/epochtalk_server/models/board_moderator.ex | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/epochtalk_server/models/board_moderator.ex b/lib/epochtalk_server/models/board_moderator.ex index 48f82520..b1f28462 100644 --- a/lib/epochtalk_server/models/board_moderator.ex +++ b/lib/epochtalk_server/models/board_moderator.ex @@ -1,11 +1,8 @@ defmodule EpochtalkServer.Models.BoardModerator do use Ecto.Schema import Ecto.Changeset - import Ecto.Query, only: [from: 2] - alias EpochtalkServer.Repo alias EpochtalkServer.Models.User alias EpochtalkServer.Models.Board - alias EpochtalkServer.Models.BoardModerator @primary_key false schema "board_moderators" do From f81d2e4038c15703733de22d5da8b8ca8048ce53 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 6 Oct 2022 10:44:09 -1000 Subject: [PATCH 183/231] refactor: misc spacing and formatting --- lib/epochtalk_server/session.ex | 2 +- lib/epochtalk_server_web/router.ex | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index 74f70e0f..b2c3b115 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -20,7 +20,7 @@ defmodule EpochtalkServer.Session do # save session save(user, session_id) - #TODO(akinsey): error handling + # TODO(akinsey): error handling # return user with token {:ok, user, encoded_token, conn} end diff --git a/lib/epochtalk_server_web/router.ex b/lib/epochtalk_server_web/router.ex index bc832127..ca95960b 100644 --- a/lib/epochtalk_server_web/router.ex +++ b/lib/epochtalk_server_web/router.ex @@ -4,13 +4,14 @@ defmodule EpochtalkServerWeb.Router do pipeline :api do plug :accepts, ["json"] end + pipeline :maybe_auth do plug Guardian.Plug.Pipeline, module: EpochtalkServer.Auth.Guardian, error_handler: EpochtalkServerWeb.GuardianErrorHandler - plug Guardian.Plug.VerifyHeader, claims: %{"typ" => "access"} plug Guardian.Plug.LoadResource, allow_blank: true end + pipeline :enforce_auth do plug Guardian.Plug.EnsureAuthenticated end @@ -19,6 +20,7 @@ defmodule EpochtalkServerWeb.Router do pipe_through [:api, :maybe_auth, :enforce_auth] get "/authenticate", AuthController, :authenticate end + scope "/api", EpochtalkServerWeb do pipe_through [:api, :maybe_auth] get "/register/username/:username", AuthController, :username @@ -35,7 +37,6 @@ defmodule EpochtalkServerWeb.Router do if Mix.env() == :dev do scope "/dev" do pipe_through [:fetch_session, :protect_from_forgery] - forward "/mailbox", Plug.Swoosh.MailboxPreview end end From 7836344d4f8de34c27f4809d19fd0dd4d4821999 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 6 Oct 2022 11:29:19 -1000 Subject: [PATCH 184/231] refactor(ban-docs): modify ban documentation to use spec instead of Returns --- lib/epochtalk_server/models/ban.ex | 52 +++++++++++++++++++----------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/lib/epochtalk_server/models/ban.ex b/lib/epochtalk_server/models/ban.ex index 4c60a28f..e6a8d012 100644 --- a/lib/epochtalk_server/models/ban.ex +++ b/lib/epochtalk_server/models/ban.ex @@ -26,9 +26,11 @@ defmodule EpochtalkServer.Models.Ban do @doc """ Create generic changeset for `Ban` model - - Returns `%EpochtalkServer.Models.Ban{}` """ + @spec changeset( + ban :: %EpochtalkServer.Models.Ban{}, + attrs :: %{} | nil + ) :: %EpochtalkServer.Models.Ban{} def changeset(ban, attrs \\ %{}) do ban |> cast(attrs, [:id, :user_id, :expiration, :created_at, :updated_at]) @@ -37,9 +39,11 @@ defmodule EpochtalkServer.Models.Ban do @doc """ Create ban changeset for `Ban` model, handles upsert of ban for banning - - Returns `%EpochtalkServer.Models.Ban{}` """ + @spec ban_changeset( + ban :: %EpochtalkServer.Models.Ban{}, + attrs :: %{} | nil + ) :: %EpochtalkServer.Models.Ban{} def ban_changeset(ban, attrs \\ %{}) do now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) attrs = attrs @@ -53,9 +57,11 @@ defmodule EpochtalkServer.Models.Ban do @doc """ Create unban changeset for `Ban` model, handles update of ban for unbanning - - Returns `%EpochtalkServer.Models.Ban{}` """ + @spec unban_changeset( + ban :: %EpochtalkServer.Models.Ban{}, + attrs :: %{} | nil + ) :: %EpochtalkServer.Models.Ban{} def unban_changeset(ban, attrs \\ %{}) do now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) attrs = attrs @@ -70,17 +76,20 @@ defmodule EpochtalkServer.Models.Ban do @doc """ Fetches `Ban` associated for a specific `User` - - Returns `%EpochtalkServer.Models.Ban{} | nil` """ + @spec by_user_id( + user_id :: integer + ) :: {:ok, ban_changeset :: Ecto.Changeset.t()} | {:error, ban_changeset :: Ecto.Changeset.t()} def by_user_id(user_id) when is_integer(user_id), do: Repo.get_by(Ban, user_id: user_id) @doc """ Used to ban a `User` by `user_id` until supplied `expiration`. Passing `nil` for `expiration` will permanently ban the `User` - - Returns `{:ok, %EpochtalkServer.Models.Ban{}} | {:error, :ban_error}` """ + @spec ban_by_user_id( + user_id :: integer, + expiration :: Calendar.naive_datetime() | nil + ) :: {:ok, ban_changeset :: Ecto.Changeset.t()} | {:error, :ban_error} def ban_by_user_id(user_id, expiration) do Repo.transaction(fn -> RoleUser.set_user_role(Role.get_banned_role_id, user_id) @@ -101,17 +110,20 @@ defmodule EpochtalkServer.Models.Ban do @doc """ Used to ban a `User` permanently. Updates supplied `User` model to reflect ban and returns. - - Returns `{:ok, %EpochtalkServer.Models.User{}}` """ + @spec ban( + user :: %EpochtalkServer.Models.User{} + ) :: {:ok, user_changeset :: Ecto.Changeset.t()} | {:error, :ban_error} def ban(%User{} = user), do: ban(user, nil) @doc """ Used to ban a `User` until supplied `expiration`. Passing `nil` for `expiration` will permanently ban the `User`. Updates supplied `User` model to reflect ban and returns. - - Returns `{:ok, %EpochtalkServer.Models.User{}} | {:error, :ban_error}` """ + @spec ban( + user :: %EpochtalkServer.Models.User{}, + expiration :: Calendar.naive_datetime() | nil + ) :: {:ok, user_changeset :: Ecto.Changeset.t()} | {:error, :ban_error} def ban(%User{ id: id} = user, expiration) do case ban_by_user_id(id, expiration) do {:ok, _ban_info} -> # successful ban, update roles/ban info on user @@ -124,10 +136,11 @@ defmodule EpochtalkServer.Models.Ban do end @doc """ - Used to unban a `User` by `user_id`. - - Returns `{:ok, %EpochtalkServer.Models.Ban{}} | {:ok, nil} | {:error, :unban_error}` + Used to unban a `User` by `user_id`. Will return `{:ok, nil}` if user was never banned. """ + @spec unban_by_user_id( + user_id :: integer + ) :: {:ok, ban_changeset :: Ecto.Changeset.t()} | {:ok, nil} | {:error, :unban_error} def unban_by_user_id(user_id) when is_integer(user_id) do Repo.transaction(fn -> RoleUser.delete_user_role(Role.get_banned_role_id, user_id) # delete ban role from user @@ -147,9 +160,10 @@ defmodule EpochtalkServer.Models.Ban do @doc """ Used to unban a `User`. Updates supplied `User` model to reflect unbanning and returns. - - Returns `{:ok, %EpochtalkServer.Models.User{}} | {:error, :unban_error}` """ + @spec unban( + user :: %EpochtalkServer.Models.User{} + ) :: {:ok, user_changeset :: Ecto.Changeset.t()} | {:error, :unban_error} def unban(%User{ban_info: %Ban{expiration: expiration}, id: user_id} = user) do if NaiveDateTime.compare(expiration, NaiveDateTime.utc_now) == :lt do case unban_by_user_id(user_id) do From 4ce988ca1beff0de29d0247329f61e6166b2b2a8 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 6 Oct 2022 11:29:19 -1000 Subject: [PATCH 185/231] docs(banned-address): documentation for banned address model --- lib/epochtalk_server/models/ban.ex | 2 +- lib/epochtalk_server/models/banned_address.ex | 72 +++++++++++++++---- 2 files changed, 58 insertions(+), 16 deletions(-) diff --git a/lib/epochtalk_server/models/ban.ex b/lib/epochtalk_server/models/ban.ex index e6a8d012..20d4a11b 100644 --- a/lib/epochtalk_server/models/ban.ex +++ b/lib/epochtalk_server/models/ban.ex @@ -7,7 +7,7 @@ defmodule EpochtalkServer.Models.Ban do alias EpochtalkServer.Models.RoleUser alias EpochtalkServer.Models.Ban @moduledoc """ - Ban model, for performing actions relating to banning + `Ban` model, for performing actions relating to banning """ # max naivedatetime, used for permanent bans diff --git a/lib/epochtalk_server/models/banned_address.ex b/lib/epochtalk_server/models/banned_address.ex index 8d108191..7e5e1996 100644 --- a/lib/epochtalk_server/models/banned_address.ex +++ b/lib/epochtalk_server/models/banned_address.ex @@ -4,6 +4,9 @@ defmodule EpochtalkServer.Models.BannedAddress do import Ecto.Query, only: [from: 2] alias EpochtalkServer.Repo alias EpochtalkServer.Models.BannedAddress + @moduledoc """ + `BannedAddress` model, for performing actions relating to banning by ip/hostname + """ @primary_key false schema "banned_addresses" do @@ -21,7 +24,14 @@ defmodule EpochtalkServer.Models.BannedAddress do ## === Changesets Functions === - def changeset(banned_address, attrs \\ %{}) do + @doc """ + Creates changeset for upsert of `BannedAddress` model + """ + @spec upsert_changeset( + banned_address :: %EpochtalkServer.Models.BannedAddress{}, + attrs :: %{} | nil + ) :: %EpochtalkServer.Models.BannedAddress{} + def upsert_changeset(banned_address, attrs \\ %{}) do now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) attrs = attrs |> Map.put(:created_at, now) @@ -34,19 +44,33 @@ defmodule EpochtalkServer.Models.BannedAddress do |> Map.put(:ip2, Enum.at(ip, 1)) |> Map.put(:ip3, Enum.at(ip, 2)) |> Map.put(:ip4, Enum.at(ip, 3)) - changeset_ip(banned_address, attrs) - _ -> changeset_hostname(banned_address, attrs) + ip_changeset(banned_address, attrs) + _ -> hostname_changeset(banned_address, attrs) end end - def changeset_hostname(banned_address, attrs \\ %{}) do + @doc """ + Creates changeset of `BannedAddress` model with hostname information + """ + @spec hostname_changeset( + banned_address :: %EpochtalkServer.Models.BannedAddress{}, + attrs :: %{} | nil + ) :: %EpochtalkServer.Models.BannedAddress{} + def hostname_changeset(banned_address, attrs \\ %{}) do banned_address |> cast(attrs, [:hostname, :weight, :decay, :imported_at, :created_at, :updates]) |> validate_required([:hostname, :weight, :decay, :created_at, :updates]) |> unique_constraint(:hostname, name: :banned_addresses_hostname_index) end - def changeset_ip(banned_address, attrs \\ %{}) do + @doc """ + Creates changeset of `BannedAddress` model with IP information + """ + @spec ip_changeset( + banned_address :: %EpochtalkServer.Models.BannedAddress{}, + attrs :: %{} | nil + ) :: %EpochtalkServer.Models.BannedAddress{} + def ip_changeset(banned_address, attrs \\ %{}) do cs_data = banned_address |> cast(attrs, [:ip1, :ip2, :ip3, :ip4, :weight, :decay, :imported_at, :created_at, :updates]) |> validate_required([:ip1, :ip2, :ip3, :ip4, :weight, :decay, :created_at, :updates]) @@ -66,12 +90,24 @@ defmodule EpochtalkServer.Models.BannedAddress do ## === Database Functions === + @doc """ + Upserts a `BannedAddress` into the database and handles calculation of weight accounting for decay + """ + @spec upsert( + banned_address_or_list :: %{} | [%{}] | %EpochtalkServer.Models.BannedAddress{} | [%EpochtalkServer.Models.BannedAddress{}] + ) :: {:ok, banned_address_changeset :: Ecto.Changeset.t()} | {:error, :banned_address_error} def upsert(address_list) when is_list(address_list) do - Repo.transaction(fn -> - Enum.each(address_list ,&upsert(&1)) - end) + Repo.transaction(fn -> Enum.each(address_list ,&upsert_one(&1)) end) + |> case do + {:ok, banned_address_changeset} -> {:ok, banned_address_changeset} + {:error, err} -> # print error, return error atom + # TODO(akinsey): handle in logger (telemetry possibly) + IO.inspect err + {:error, :banned_address_error} + end end - def upsert(banned_address) do + def upsert(banned_address), do: upsert([banned_address]) + defp upsert_one(banned_address) do case Map.get(banned_address, :hostname) do # IP type banned address nil -> @@ -82,7 +118,7 @@ defmodule EpochtalkServer.Models.BannedAddress do ip4 = Enum.at(ip, 3) db_banned_address = Repo.get_by(BannedAddress, %{ip1: ip1, ip2: ip2, ip3: ip3, ip4: ip4}) if db_banned_address do # update - cs_data = changeset(db_banned_address, banned_address) + cs_data = upsert_changeset(db_banned_address, banned_address) updated_cs = Map.merge(cs_data.data, cs_data.changes) from(ba in BannedAddress, where: ba.ip1 == ^ip1 and ba.ip2 == ^ip2 and ba.ip3 == ^ip3 and ba.ip4 == ^ip4) |> Repo.update_all(set: [ @@ -91,13 +127,13 @@ defmodule EpochtalkServer.Models.BannedAddress do updates: Map.get(updated_cs, :updates) ]) {:ok, updated_cs} - else Repo.insert(changeset(%BannedAddress{}, banned_address), returning: true) end #insert + else Repo.insert(upsert_changeset(%BannedAddress{}, banned_address), returning: true) end #insert # hostname type banned address hostname -> if db_banned_address = Repo.get_by(BannedAddress, hostname: hostname) do # update # grab changes from changeset, this is a workaround since # we can't use Repo.update, because there is no primary key - cs_data = changeset(db_banned_address, banned_address) + cs_data = upsert_changeset(db_banned_address, banned_address) updated_cs = Map.merge(cs_data.data, cs_data.changes) from(ba in BannedAddress, where: ba.hostname == ^hostname) |> Repo.update_all(set: [ @@ -106,18 +142,24 @@ defmodule EpochtalkServer.Models.BannedAddress do updates: Map.get(updated_cs, :updates) ]) {:ok, updated_cs} - else Repo.insert(changeset(%BannedAddress{}, banned_address), returning: true) end # insert + else Repo.insert(upsert_changeset(%BannedAddress{}, banned_address), returning: true) end # insert end end ## === External Helper Functions === - def calculate_malicious_score_from_ip(ip) do + @doc """ + Calculates the malicious score of the provided IP address, `float` score is returned if IP/Hostname are malicious, otherwise nil + """ + @spec calculate_malicious_score_from_ip( + ip_address :: String.t() + ) :: float | nil + def calculate_malicious_score_from_ip(ip) when is_binary(ip) do case :inet.parse_address(to_charlist(ip)) do {:ok, ip} -> hostname_score = case :inet_res.gethostbyaddr(ip) do {:ok, host} -> hostname_from_host(host) |> calculate_hostname_score - {:error, _} -> nil # no hostname found, return nil for hostname score + {:error, _} -> 0 # no hostname found, return nil for hostname score end ip32_score = calculate_ip32_score(ip) ip24_score = calculate_ip24_score(ip) From 8d3a34b1a536ae4ba4f69b94e5cb7a9ecf43ebd9 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 6 Oct 2022 11:57:20 -1000 Subject: [PATCH 186/231] refactor(boards): board create now returns :ok tuple with board --- lib/epochtalk_server/models/board.ex | 10 +++++++--- priv/repo/seed_forum.exs | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/epochtalk_server/models/board.ex b/lib/epochtalk_server/models/board.ex index 5aa8a538..ff4f1177 100644 --- a/lib/epochtalk_server/models/board.ex +++ b/lib/epochtalk_server/models/board.ex @@ -45,13 +45,17 @@ defmodule EpochtalkServer.Models.Board do ## === Database Functions === - def insert(%Board{} = board), do: Repo.insert(board) - + @doc """ + Creates a new `Board` in the database + """ + @spec create( + board_attrs :: %{} + ) :: {:ok, board :: %EpochtalkServer.Models.Board{}} | {:error, Ecto.Changeset.t()} def create(board) do board_cs = create_changeset(%Board{}, board) case Repo.insert(board_cs) do {:ok, db_board} -> case MetadataBoard.insert(%MetadataBoard{ board_id: db_board.id}) do - {:ok, _} -> db_board + {:ok, _} -> {:ok, db_board} {:error, cs} -> {:error, cs} end {:error, cs} -> {:error, cs} diff --git a/priv/repo/seed_forum.exs b/priv/repo/seed_forum.exs index 588a9007..e579ff8e 100644 --- a/priv/repo/seed_forum.exs +++ b/priv/repo/seed_forum.exs @@ -22,7 +22,7 @@ Repo.transaction(fn -> board_id = Board.create(board) |> case do - b -> b.id + {:ok, b} -> b.id end board_mapping = [ From 49069c52bce6e713b8708e180ab1835a62585242 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 6 Oct 2022 13:16:11 -1000 Subject: [PATCH 187/231] docs(board): add module docs for boards --- lib/epochtalk_server/models/board.ex | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/epochtalk_server/models/board.ex b/lib/epochtalk_server/models/board.ex index ff4f1177..425dead8 100644 --- a/lib/epochtalk_server/models/board.ex +++ b/lib/epochtalk_server/models/board.ex @@ -6,6 +6,9 @@ defmodule EpochtalkServer.Models.Board do alias EpochtalkServer.Models.BoardMapping alias EpochtalkServer.Models.Board alias EpochtalkServer.Models.MetadataBoard + @moduledoc """ + `Board` model, for performing actions relating to forum boards + """ schema "boards" do field :name, :string @@ -25,6 +28,13 @@ defmodule EpochtalkServer.Models.Board do ## === Changesets Functions === + @doc """ + Create generic changeset for `Board` model + """ + @spec changeset( + board :: %EpochtalkServer.Models.Board{}, + attrs :: %{} | nil + ) :: %EpochtalkServer.Models.Board{} def changeset(board, attrs) do board |> cast(attrs, [:id, :name, :slug, :description, :post_count, :thread_count, :viewable_by, :postable_by, :created_at, :imported_at, :updated_at, :meta]) @@ -32,6 +42,16 @@ defmodule EpochtalkServer.Models.Board do |> unique_constraint(:id, name: :boards_pkey) |> unique_constraint(:slug, name: :boards_slug_index) end + + ## === Changesets Functions === + + @doc """ + Create changeset for creation of `Board` model + """ + @spec create_changeset( + board :: %EpochtalkServer.Models.Board{}, + attrs :: %{} | nil + ) :: %EpochtalkServer.Models.Board{} def create_changeset(board, attrs) do now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) attrs = attrs From b5915d5b73171dc06995d8727d3fdf07b1d25530 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 6 Oct 2022 14:23:21 -1000 Subject: [PATCH 188/231] docs(board-mapping): add docs for board mapping model --- lib/epochtalk_server/models/board_mapping.ex | 39 ++++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/lib/epochtalk_server/models/board_mapping.ex b/lib/epochtalk_server/models/board_mapping.ex index 01dadd50..16829721 100644 --- a/lib/epochtalk_server/models/board_mapping.ex +++ b/lib/epochtalk_server/models/board_mapping.ex @@ -6,6 +6,9 @@ defmodule EpochtalkServer.Models.BoardMapping do alias EpochtalkServer.Models.Board alias EpochtalkServer.Models.BoardMapping alias EpochtalkServer.Models.Category + @moduledoc """ + `BoardMapping` model, for performing actions relating to mapping forum boards and categories + """ @primary_key false schema "board_mapping" do @@ -17,6 +20,13 @@ defmodule EpochtalkServer.Models.BoardMapping do ## === Changesets Functions === + @doc """ + Create generic changeset for `BoardMapping` model + """ + @spec changeset( + board_mapping :: %EpochtalkServer.Models.BoardMapping{}, + attrs :: %{} | nil + ) :: %EpochtalkServer.Models.BoardMapping{} def changeset(board_mapping, attrs) do board_mapping |> cast(attrs, [:board_id, :parent_id, :category_id, :view_order]) @@ -27,23 +37,36 @@ defmodule EpochtalkServer.Models.BoardMapping do ## === Database Functions === - def insert(%BoardMapping{} = board_mapping), do: Repo.insert(board_mapping) - - def delete_board(id) do + @doc """ + Deletes a `Board` from the `BoardMapping` + """ + @spec delete_board_by_id( + board_id :: integer() + ) :: {non_neg_integer(), nil | [term()]} + def delete_board_by_id(id) when is_integer(id) do query = from bm in BoardMapping, where: bm.board_id == ^id Repo.delete_all(query) end + @doc """ + Updates `BoardMapping` in the database + """ + @spec update( + board_mapping_list :: [%{}] + ) :: {:ok, Ecto.Changeset.t()} | {:error, Ecto.Changeset.t()}| {:error, any()} def update(board_mapping_list) do Repo.transaction(fn -> - Enum.each(board_mapping_list, &BoardMapping.update(&1, Map.get(&1, :type))) + Enum.each(board_mapping_list, &update(&1, Map.get(&1, :type))) end) end - def update(cat, "category"), do: Category.update_for_board_mapping(cat) - def update(uncat, "uncategorized"), do: delete_board(Map.get(uncat, :id)) - def update(board, "board") do - delete_board(Map.get(board, :id)) + + ## === Private Helper Functions === + + defp update(cat, "category"), do: Category.update_for_board_mapping(cat) + defp update(uncat, "uncategorized"), do: BoardMapping.delete_board_by_id(Map.get(uncat, :id)) + defp update(board, "board") do + BoardMapping.delete_board_by_id(Map.get(board, :id)) %BoardMapping{} |> changeset(Map.put(board, :board_id, Map.get(board, :id))) |> Repo.insert From cba4507c5d62eccb6a68e8e4121cf8c6a349cbcb Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 6 Oct 2022 16:35:19 -1000 Subject: [PATCH 189/231] docs(board-moderator): add docs for board moderator model --- lib/epochtalk_server/models/board_moderator.ex | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server/models/board_moderator.ex b/lib/epochtalk_server/models/board_moderator.ex index b1f28462..5a29deb8 100644 --- a/lib/epochtalk_server/models/board_moderator.ex +++ b/lib/epochtalk_server/models/board_moderator.ex @@ -3,6 +3,9 @@ defmodule EpochtalkServer.Models.BoardModerator do import Ecto.Changeset alias EpochtalkServer.Models.User alias EpochtalkServer.Models.Board + @moduledoc """ + `BoardModerator` model, for performing actions relating to `Board` moderators + """ @primary_key false schema "board_moderators" do @@ -12,8 +15,15 @@ defmodule EpochtalkServer.Models.BoardModerator do ## === Changesets Functions === - def changeset(permission, attrs \\ %{}) do - permission + @doc """ + Create generic changeset for `BoardModerator` model + """ + @spec changeset( + board_moderator :: %EpochtalkServer.Models.BoardModerator{}, + attrs :: %{} | nil + ) :: %EpochtalkServer.Models.Board{} + def changeset(board_moderator, attrs \\ %{}) do + board_moderator |> cast(attrs, [:user_id, :board_id]) |> validate_required([:user_id, :board_id]) end From 847dfbbba2c947a4f3336b94d462f5ac7ecb97f8 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 6 Oct 2022 16:35:39 -1000 Subject: [PATCH 190/231] docs(category): add docs for category model --- lib/epochtalk_server/models/category.ex | 40 +++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/lib/epochtalk_server/models/category.ex b/lib/epochtalk_server/models/category.ex index f7f0ece5..df82abd7 100644 --- a/lib/epochtalk_server/models/category.ex +++ b/lib/epochtalk_server/models/category.ex @@ -5,6 +5,9 @@ defmodule EpochtalkServer.Models.Category do alias EpochtalkServer.Models.Board alias EpochtalkServer.Models.Category alias EpochtalkServer.Models.BoardMapping + @moduledoc """ + `Category` model, for performing actions relating to forum categories + """ schema "categories" do field :name, :string @@ -20,12 +23,26 @@ defmodule EpochtalkServer.Models.Category do ## === Changesets Functions === + @doc """ + Create generic changeset for `Category` model + """ + @spec changeset( + category :: %EpochtalkServer.Models.Category{}, + attrs :: %{} | nil + ) :: %EpochtalkServer.Models.Category{} def changeset(category, attrs) do category |> cast(attrs, [:id, :name, :view_order, :viewable_by, :postable_by, :created_at, :imported_at, :updated_at, :meta]) |> unique_constraint(:id, name: :categories_pkey) end + @doc """ + Creates changeset for inserting a new `Category` model + """ + @spec create_changeset( + category :: %EpochtalkServer.Models.Category{}, + attrs :: %{} | nil + ) :: %EpochtalkServer.Models.Category{} def create_changeset(category, attrs) do now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) attrs = attrs @@ -35,6 +52,13 @@ defmodule EpochtalkServer.Models.Category do |> cast(attrs, [:name, :viewable_by, :created_at, :updated_at]) end + @doc """ + Creates changeset for updating an existing `Category` model + """ + @spec create_changeset( + category :: %EpochtalkServer.Models.Category{}, + attrs :: %{} | nil + ) :: %EpochtalkServer.Models.Category{} def update_for_board_mapping_changeset(category, attrs) do category |> cast(attrs, [:id, :name, :view_order, :viewable_by]) @@ -43,13 +67,23 @@ defmodule EpochtalkServer.Models.Category do ## === Database Functions === - def insert(%Category{} = category), do: Repo.insert(category) - + @doc """ + Creates a new `Category` in the database + """ + @spec create( + category_attrs :: %{} + ) :: {:ok, category :: %EpochtalkServer.Models.Category{}} | {:error, Ecto.Changeset.t()} def create(attrs) do category_cs = create_changeset(%Category{}, attrs) - Repo.insert!(category_cs) + Repo.insert(category_cs) end + @doc """ + Updates an existing `Category` in the database, used by board mapping to recategorize boards + """ + @spec update_for_board_mapping( + category_map :: %{ id: id :: integer } + ) :: {:ok, category :: %EpochtalkServer.Models.Category{}} | {:error, Ecto.Changeset.t()} def update_for_board_mapping(%{ id: id } = category_map) do %Category{ id: id } |> update_for_board_mapping_changeset(category_map) From 505ab4b87d7b3238236dc002577e6c4a080ccf79 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 6 Oct 2022 16:35:59 -1000 Subject: [PATCH 191/231] docs(invitation): add docs for invitation model --- lib/epochtalk_server/models/invitation.ex | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/epochtalk_server/models/invitation.ex b/lib/epochtalk_server/models/invitation.ex index bc4e252f..b050bcf2 100644 --- a/lib/epochtalk_server/models/invitation.ex +++ b/lib/epochtalk_server/models/invitation.ex @@ -4,6 +4,9 @@ defmodule EpochtalkServer.Models.Invitation do import Ecto.Query, only: [from: 2] alias EpochtalkServer.Repo alias EpochtalkServer.Models.Invitation + @moduledoc """ + `Invitation` model, for performing actions relating to inviting new users to the forum + """ @primary_key false schema "invitations" do @@ -14,6 +17,13 @@ defmodule EpochtalkServer.Models.Invitation do ## === Changesets Functions === + @doc """ + Create changeset for inserting a new `Invitation` model + """ + @spec create_changeset( + invitation :: %EpochtalkServer.Models.Invitation{}, + attrs :: %{} | nil + ) :: %EpochtalkServer.Models.Invitation{} def create_changeset(invitation, attrs \\ %{}) do now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) attrs = attrs @@ -31,7 +41,19 @@ defmodule EpochtalkServer.Models.Invitation do ## === Database Functions === + @doc """ + Creates a new `Invitation` in the database + """ + @spec create( + email :: String.t() + ) :: {:ok, invitation :: %EpochtalkServer.Models.Invitation{}} | {:error, Ecto.Changeset.t()} def create(email), do: Repo.insert(create_changeset(%Invitation{}, %{email: email})) + @doc """ + Deletes `Invitation` from the database by email + """ + @spec delete( + email :: String.t() + ) :: {non_neg_integer(), nil | [term()]} def delete(email), do: Repo.delete_all(from(i in Invitation, where: i.email == ^email)) end From e746e5425aec29c1f6fe097c0134a3f19a44b412 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 6 Oct 2022 16:36:25 -1000 Subject: [PATCH 192/231] docs(metadata-board): add docs for metadata board model --- lib/epochtalk_server/models/metadata_board.ex | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/epochtalk_server/models/metadata_board.ex b/lib/epochtalk_server/models/metadata_board.ex index ee469ee3..ecb69f87 100644 --- a/lib/epochtalk_server/models/metadata_board.ex +++ b/lib/epochtalk_server/models/metadata_board.ex @@ -4,6 +4,9 @@ defmodule EpochtalkServer.Models.MetadataBoard do alias EpochtalkServer.Repo alias EpochtalkServer.Models.Board alias EpochtalkServer.Models.MetadataBoard + @moduledoc """ + `MetadataBoard` model, for performing actions relating to `Board` metadata + """ @schema_prefix "metadata" schema "boards" do @@ -21,8 +24,15 @@ defmodule EpochtalkServer.Models.MetadataBoard do ## === Changesets Functions === - def changeset(permission, attrs \\ %{}) do - permission + @doc """ + Create changeset for inserting a new `MetadataBoard` model + """ + @spec changeset( + metadata_board :: %EpochtalkServer.Models.MetadataBoard{}, + attrs :: %{} | nil + ) :: %EpochtalkServer.Models.MetadataBoard{} + def changeset(metadata_board, attrs \\ %{}) do + metadata_board |> cast(attrs, [:id, :board_id, :post_count, :thread_count, :total_post, :total_thread_count, :last_post_username, :last_post_created_at, :last_thread_id, :last_thread_title, @@ -32,5 +42,11 @@ defmodule EpochtalkServer.Models.MetadataBoard do ## === Database Functions === - def insert(%MetadataBoard{} = metadata_boards), do: Repo.insert(metadata_boards) + @doc """ + Inserts a new `MetadataBoard` into the database + """ + @spec insert( + metadata_board :: %EpochtalkServer.Models.MetadataBoard{} + ) :: {:ok, metadata_board :: %EpochtalkServer.Models.MetadataBoard{}} | {:error, Ecto.Changeset.t()} + def insert(%MetadataBoard{} = metadata_board), do: Repo.insert(metadata_board) end From a934fd68f1a4e81d208ff727f1a4db0afe6ea7d8 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 6 Oct 2022 16:36:52 -1000 Subject: [PATCH 193/231] docs(permission): add docs for permission model --- lib/epochtalk_server/models/permission.ex | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/epochtalk_server/models/permission.ex b/lib/epochtalk_server/models/permission.ex index 15cdef93..1f87320e 100644 --- a/lib/epochtalk_server/models/permission.ex +++ b/lib/epochtalk_server/models/permission.ex @@ -3,6 +3,9 @@ defmodule EpochtalkServer.Models.Permission do import Ecto.Changeset alias EpochtalkServer.Repo alias EpochtalkServer.Models.Permission + @moduledoc """ + `Permission` model, for performing actions relating to `Role` permissions, used for seeding + """ @primary_key false schema "permissions" do @@ -11,6 +14,13 @@ defmodule EpochtalkServer.Models.Permission do ## === Changesets Functions === + @doc """ + Creates a generic changeset for `Permission` model + """ + @spec changeset( + permission :: %EpochtalkServer.Models.Permission{}, + attrs :: %{} | nil + ) :: %EpochtalkServer.Models.Permission{} def changeset(permission, attrs \\ %{}) do permission |> cast(attrs, [:path]) @@ -19,7 +29,15 @@ defmodule EpochtalkServer.Models.Permission do ## === Database Functions === + @doc """ + Returns every `Permission` record in the database + """ + @spec all() :: [%EpochtalkServer.Models.Permission{}] | [] def all(), do: Repo.all(Permission) + @doc """ + Returns a specific `Permission` provided it's path + """ + @spec by_path(path :: String.t()) :: %EpochtalkServer.Models.Permission{} | nil def by_path(path) when is_binary(path), do: Repo.get_by(Permission, path: path) end From 3a8100b9533ebc81bfc60b9dd64bb6c5b2561ac7 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 6 Oct 2022 16:37:14 -1000 Subject: [PATCH 194/231] docs(preference): add docs for preference model --- lib/epochtalk_server/models/preference.ex | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server/models/preference.ex b/lib/epochtalk_server/models/preference.ex index ff458377..2ba7ab5e 100644 --- a/lib/epochtalk_server/models/preference.ex +++ b/lib/epochtalk_server/models/preference.ex @@ -2,6 +2,9 @@ defmodule EpochtalkServer.Models.Preference do use Ecto.Schema import Ecto.Changeset alias EpochtalkServer.Models.User + @moduledoc """ + `Preference` model, for performing actions relating to a user's preferences + """ @primary_key false @schema_prefix "users" @@ -21,8 +24,15 @@ defmodule EpochtalkServer.Models.Preference do ## === Changesets Functions === - def changeset(permission, attrs \\ %{}) do - permission + @doc """ + Creates a generic changeset for `Preference` model + """ + @spec changeset( + preference :: %EpochtalkServer.Models.Preference{}, + attrs :: %{} | nil + ) :: %EpochtalkServer.Models.Preference{} + def changeset(preference, attrs \\ %{}) do + preference |> cast(attrs, [:user_id, :posts_per_page, :threads_per_page, :collapsed_categories, :ignored_boards, :timezone_offset, :notify_replied_threads, :ignore_newbies, :patroller_view, From 593bf6f7e1eff8d442231bbdf7f8ad506aeaf84f Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 6 Oct 2022 16:37:38 -1000 Subject: [PATCH 195/231] docs(profile): add docs for profile model --- lib/epochtalk_server/models/profile.ex | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server/models/profile.ex b/lib/epochtalk_server/models/profile.ex index 4444f33d..c1840188 100644 --- a/lib/epochtalk_server/models/profile.ex +++ b/lib/epochtalk_server/models/profile.ex @@ -2,6 +2,9 @@ defmodule EpochtalkServer.Models.Profile do use Ecto.Schema import Ecto.Changeset alias EpochtalkServer.Models.User + @moduledoc """ + `Profile` model, for performing actions relating a user's profile + """ @schema_prefix "users" schema "profiles" do @@ -17,8 +20,15 @@ defmodule EpochtalkServer.Models.Profile do ## === Changesets Functions === - def changeset(permission, attrs \\ %{}) do - permission + @doc """ + Creates a generic changeset for `Profile` model + """ + @spec changeset( + profile :: %EpochtalkServer.Models.Profile{}, + attrs :: %{} | nil + ) :: %EpochtalkServer.Models.Profile{} + def changeset(profile, attrs \\ %{}) do + profile |> cast(attrs, [:user_id, :avatar, :position, :signature, :raw_signature, :post_count, :field, :last_active]) |> validate_required([:user_id]) end From 4c47175e3d1b41849e495be105688368c061ae93 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 6 Oct 2022 16:38:33 -1000 Subject: [PATCH 196/231] refactor(seed-forum): update seed forum to reflect changes to Category --- priv/repo/seed_forum.exs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/priv/repo/seed_forum.exs b/priv/repo/seed_forum.exs index e579ff8e..1fb5c485 100644 --- a/priv/repo/seed_forum.exs +++ b/priv/repo/seed_forum.exs @@ -18,12 +18,11 @@ board = %{ } Repo.transaction(fn -> - category_id = Category.create(category).id + category_id = Category.create(category) + |> case do {:ok, c} -> c.id end board_id = Board.create(board) - |> case do - {:ok, b} -> b.id - end + |> case do {:ok, b} -> b.id end board_mapping = [ %{ From d309957d222d4b0f48bf980f29ef594876a3d737 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Fri, 7 Oct 2022 10:41:52 -1000 Subject: [PATCH 197/231] test(error-handling): fix tests so that they pass with new error handling style --- lib/epochtalk_server_web/views/error_view.ex | 25 +++++++++++++------ .../views/error_view_test.exs | 13 +++++++--- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/lib/epochtalk_server_web/views/error_view.ex b/lib/epochtalk_server_web/views/error_view.ex index 95816076..4858f3e1 100644 --- a/lib/epochtalk_server_web/views/error_view.ex +++ b/lib/epochtalk_server_web/views/error_view.ex @@ -4,20 +4,31 @@ defmodule EpochtalkServerWeb.ErrorView do # By default, Phoenix returns the status message from # the template name. For example, "404.json" becomes # "Not Found". - def template_not_found(_template, assigns), do: format_error(assigns) + + def template_not_found(template, assigns), do: format_error(template, assigns) # match assigns.reason.message and assigns.conn.status - defp format_error(%{reason: %{message: message}, conn: %{status: status}}), do: format_error(status, message) + defp format_error(_template, %{reason: %{message: message}, conn: %{status: status}}), do: format_error(status, message) # match assigns.reason.message and assigns.status - defp format_error(%{reason: %{message: message}, status: status}), do: format_error(status, message) + defp format_error(_template, %{reason: %{message: message}, status: status}), do: format_error(status, message) # match assigns.message and assigns.conn.status - defp format_error(%{message: message, conn: %{status: status}}), do: format_error(status, message) + defp format_error(_template, %{message: message, conn: %{status: status}}), do: format_error(status, message) # match assigns.message and assigns.status - defp format_error(%{message: message, status: status}), do: format_error(status, message) + defp format_error(_template, %{message: message, status: status}), do: format_error(status, message) # match stack and status (Ex: function error from redis in session) - defp format_error(%{stack: _, status: status}), do: format_error(status, "Something went wrong") + defp format_error(_template, %{stack: _, status: status}), do: format_error(status, "Something went wrong") + # match assigns.reason.message and assigns.conn.status + defp format_error(_template, %{reason: %{message: message}, conn: %{status: status}}), do: format_error(status, message) + # match missing template with no assigns, handles render("404.json") etc + defp format_error(template, %{__phx_template_not_found__: _}) do + [potential_status | _] = String.split(template, ".json") + case Integer.parse(potential_status) do + {status, ""} -> format_error(status, "Request Error") # render error wtih status + _ -> format_error(500, "Request Error") # 500 if cannot render template + end + end # format errors with readable message - defp format_error(status, message) when is_binary(message) do + defp format_error(status, message) when is_integer(status) and is_binary(message) do %{ status: status, message: message, diff --git a/test/epochtalk_server_web/views/error_view_test.exs b/test/epochtalk_server_web/views/error_view_test.exs index 63400fc3..e87d799b 100644 --- a/test/epochtalk_server_web/views/error_view_test.exs +++ b/test/epochtalk_server_web/views/error_view_test.exs @@ -5,11 +5,18 @@ defmodule EpochtalkServerWeb.ErrorViewTest do import Phoenix.View test "renders 404.json" do - assert render(EpochtalkServerWeb.ErrorView, "404.json", []) == %{errors: %{detail: "Not Found"}} + assert render(EpochtalkServerWeb.ErrorView, "404.json", []) == %{ + error: "Not Found", + message: "Request Error", + status: 404 + } end test "renders 500.json" do - assert render(EpochtalkServerWeb.ErrorView, "500.json", []) == - %{errors: %{detail: "Internal Server Error"}} + assert render(EpochtalkServerWeb.ErrorView, "500.json", []) == %{ + error: "Internal Server Error", + message: "Request Error", + status: 500 + } end end From 2e5ed10459630da580d694882ccd63834d71bd09 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Fri, 7 Oct 2022 12:05:00 -1000 Subject: [PATCH 198/231] docs(role-permission): add docs to role permissions model --- .../models/role_permission.ex | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/lib/epochtalk_server/models/role_permission.ex b/lib/epochtalk_server/models/role_permission.ex index 3f518174..5aed7c61 100644 --- a/lib/epochtalk_server/models/role_permission.ex +++ b/lib/epochtalk_server/models/role_permission.ex @@ -6,6 +6,9 @@ defmodule EpochtalkServer.Models.RolePermission do alias EpochtalkServer.Models.RolePermission alias EpochtalkServer.Models.Role alias EpochtalkServer.Models.Permission + @moduledoc """ + `RolePermission` model, for performing actions relating to a roles permissions + """ @primary_key false schema "roles_permissions" do @@ -20,6 +23,13 @@ defmodule EpochtalkServer.Models.RolePermission do ## === Changesets Functions === + @doc """ + Creates a generic changeset for `RolePermission` model + """ + @spec changeset( + role_permission :: %EpochtalkServer.Models.RolePermission{}, + attrs :: %{} | nil + ) :: %EpochtalkServer.Models.RolePermission{} def changeset(role_permission, attrs \\ %{}) do role_permission |> cast(attrs, [:role_id, :permission_path, :value, :modified]) @@ -28,6 +38,12 @@ defmodule EpochtalkServer.Models.RolePermission do ## === Database Functions === + @doc """ + Inserts a new `RolePermission` into the database + """ + @spec insert( + role_permission_or_role_permissions :: %EpochtalkServer.Models.RolePermission{} | [%{}] + ) :: {:ok, role :: %EpochtalkServer.Models.RolePermission{}} | {non_neg_integer(), nil | [term()]} | {:error, Ecto.Changeset.t()} def insert([]), do: {:error, "Role permission list is empty"} def insert(%RolePermission{} = role_permission), do: Repo.insert(role_permission) def insert([%{}|_] = roles_permissions), do: Repo.insert_all(RolePermission, roles_permissions) @@ -52,6 +68,12 @@ defmodule EpochtalkServer.Models.RolePermission do # # update roles table # end + @doc """ + Used to update the value of a `RolePermission` in the database, if it exists or created it, if it doesnt + """ + @spec upsert_value( + role_permissions :: [%{}] + ) :: {non_neg_integer(), nil | [term()]} # change the default values of roles permissions def upsert_value([]), do: {:error, "Role permission list is empty"} def upsert_value([%{}|_] = roles_permissions) do @@ -63,7 +85,12 @@ defmodule EpochtalkServer.Models.RolePermission do ) end - # derives a single nested map of all permissions for a role + @doc """ + Derives a single nested map of all permissions for a role + """ + @spec permissions_map_by_role_id( + role_id :: integer + ) :: %{} def permissions_map_by_role_id(role_id) do from(rp in RolePermission, where: rp.role_id == ^role_id) @@ -75,9 +102,13 @@ defmodule EpochtalkServer.Models.RolePermission do |> Iteraptor.from_flatmap end - # for server-side role-loading use, only runs if roles permissions table is currently empty - # sets all roles permissions to value: false, modified: false - def maybe_init! do + @doc """ + Sets all roles permissions to value: false, modified: false + + For server-side role-loading use, only runs if roles permissions table is currently empty + """ + @spec maybe_init!() :: [%RolePermission{}] | nil + def maybe_init!() do if Repo.one(from rp in RolePermission, select: count(rp.value)) == 0, do: Enum.each(Role.all, fn role -> Enum.each(Permission.all, fn permission -> %RolePermission{} From 6fb840fa37a80b35d74411844b96c87a6d17af7e Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Fri, 7 Oct 2022 12:05:26 -1000 Subject: [PATCH 199/231] docs(role-user): add docs to role user model --- lib/epochtalk_server/models/role_user.ex | 34 ++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server/models/role_user.ex b/lib/epochtalk_server/models/role_user.ex index e0891cfe..f93d706a 100644 --- a/lib/epochtalk_server/models/role_user.ex +++ b/lib/epochtalk_server/models/role_user.ex @@ -6,8 +6,11 @@ defmodule EpochtalkServer.Models.RoleUser do alias EpochtalkServer.Models.User alias EpochtalkServer.Models.Role alias EpochtalkServer.Models.RoleUser - @admin_role_id 1 + @moduledoc """ + `RoleUser` model, for performing actions relating a setting a `Role` for a `User` + """ + @admin_role_id 1 @primary_key false schema "roles_users" do belongs_to :user, User @@ -16,6 +19,13 @@ defmodule EpochtalkServer.Models.RoleUser do ## === Changesets Functions === + @doc """ + Creates a generic changeset for `RoleUser` model + """ + @spec changeset( + role_user :: %EpochtalkServer.Models.RoleUser{}, + attrs :: %{} | nil + ) :: %EpochtalkServer.Models.RoleUser{} def changeset(role_user, attrs \\ %{}) do role_user |> cast(attrs, [:user_id, :role_id]) @@ -24,15 +34,35 @@ defmodule EpochtalkServer.Models.RoleUser do ## === Database Functions === + @doc """ + Assigns a specific `User` to have the `superAdministrator` `Role` + """ + @spec set_admin( + user_id :: integer + ) :: {:ok, role_user :: %EpochtalkServer.Models.RoleUser{}} | {:error, Ecto.Changeset.t()} def set_admin(user_id), do: set_user_role(@admin_role_id, user_id) + @doc """ + Assigns a specific `User` to have the specified `Role` + """ + @spec set_user_role( + role_id :: integer, + user_id :: integer + ) :: {:ok, role_user :: %EpochtalkServer.Models.RoleUser{}} | {:error, Ecto.Changeset.t()} def set_user_role(role_id, user_id) do case Repo.one(from(ru in RoleUser, where: ru.role_id == ^role_id and ru.user_id == ^user_id)) do nil -> Repo.insert(changeset(%RoleUser{}, %{role_id: role_id, user_id: user_id})) - roleuser_cs -> {:ok, roleuser_cs} + role_user -> {:ok, role_user} end end + @doc """ + Removes specified `Role` from specified `User` + """ + @spec delete_user_role( + role_id :: integer, + user_id :: integer + ) :: {non_neg_integer(), nil | [term()]} def delete_user_role(role_id, user_id) do query = from ru in RoleUser, where: ru.role_id == ^role_id and ru.user_id == ^user_id From 376b6018abe6b5826d3e6934df29ee38c72c746f Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Fri, 7 Oct 2022 16:32:20 -1000 Subject: [PATCH 200/231] docs(role): add docs to role model --- lib/epochtalk_server/models/role.ex | 71 ++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/lib/epochtalk_server/models/role.ex b/lib/epochtalk_server/models/role.ex index 62d84430..1d4a69e5 100644 --- a/lib/epochtalk_server/models/role.ex +++ b/lib/epochtalk_server/models/role.ex @@ -6,6 +6,9 @@ defmodule EpochtalkServer.Models.Role do alias EpochtalkServer.Models.User alias EpochtalkServer.Models.Role alias EpochtalkServer.Models.RoleUser + @moduledoc """ + `Role` model, for performing actions relating to user roles + """ @derive {Jason.Encoder, only: [:name, :description, :lookup, :priority, :highlight_color, :permissions, :priority_restrictions, :created_at, :updated_at]} schema "roles" do @@ -24,6 +27,13 @@ defmodule EpochtalkServer.Models.Role do ## === Changesets Functions === + @doc """ + Create generic changeset for the `Role` model + """ + @spec changeset( + role :: %EpochtalkServer.Models.Role{}, + attrs :: %{} | nil + ) :: %EpochtalkServer.Models.Role{} def changeset(role, attrs \\ %{}) do role |> cast(attrs, [:name, :description, :lookup, :priority, :permissions]) @@ -34,19 +44,47 @@ defmodule EpochtalkServer.Models.Role do ## SELECT OPERATIONS + @doc """ + Returns every `Role` record in the database + """ + @spec all() :: [%EpochtalkServer.Models.Role{}] | [] def all, do: from(r in Role, order_by: r.id) |> Repo.all + @doc """ + Returns id for the `banned` `Role` + """ + @spec get_banned_role_id() :: integer | nil def get_banned_role_id(), do: Repo.one(from(r in Role, select: r.id, where: r.lookup == "banned")) + @doc """ + Returns id for the `newbie` `Role` + """ + @spec get_newbie_role_id() :: integer | nil def get_newbie_role_id(), do: Repo.one(from(r in Role, select: r.id, where: r.lookup == "newbie")) + @doc """ + Returns default `Role`, for base installation this is the `user` role + """ + @spec get_default() :: %EpochtalkServer.Models.Role{} | nil def get_default(), do: by_lookup("user") + @doc """ + Returns a `Role` or list of roles, for specified lookup(s) + """ + @spec by_lookup( + lookup_or_lookups :: String.t() | [String.t()] + ) :: %EpochtalkServer.Models.Role{} | [%EpochtalkServer.Models.Role{}] | [] | nil def by_lookup(lookups) when is_list(lookups) do from(r in Role, where: r.lookup in ^lookups) |> Repo.all end def by_lookup(lookup), do: Repo.get_by(Role, lookup: lookup) + @doc """ + Returns a list containing a user's roles + """ + @spec by_user_id( + user_id :: integer + ) :: [%EpochtalkServer.Models.Role{}] def by_user_id(user_id) do query = from ru in RoleUser, join: r in Role, @@ -63,12 +101,26 @@ defmodule EpochtalkServer.Models.Role do ## CREATE OPERATIONS # For seeding roles + + @doc """ + Inserts a new `Role` into the database + """ + @spec insert( + role_or_roles :: %EpochtalkServer.Models.Role{} | [%{}] + ) :: {:ok, role :: %EpochtalkServer.Models.Role{}} | {non_neg_integer(), nil | [term()]} | {:error, Ecto.Changeset.t()} def insert([]), do: {:error, "Role list is empty"} def insert(%Role{} = role), do: Repo.insert(role) def insert([%{}|_] = roles), do: Repo.insert_all(Role, roles) ## UPDATE OPERATIONS + @doc """ + Updates the permissions of an existing `Role` in the database + """ + @spec set_permissions( + id :: integer, + permissions_attrs :: %{} + ) :: {:ok, role :: %EpochtalkServer.Models.Role{}} | {:error, Ecto.Changeset.t()} def set_permissions(id, permissions) do Role |> Repo.get(id) @@ -80,11 +132,16 @@ defmodule EpochtalkServer.Models.Role do @doc """ - default role is not stored in the database, in order to save space + Default role is not stored in the database, in order to save space checks the role array on the user model if roles array is empty, sets the default role by appending it + This helper needs to be called anywhere that modifies a user's roles + and is expected to return the updated user's roles. """ + @spec handle_empty_user_roles( + user :: %EpochtalkServer.Models.User{} + ) :: %EpochtalkServer.Models.User{} def handle_empty_user_roles(%User{roles: [%Role{} | _]} = user), do: user def handle_empty_user_roles(%User{roles: []} = user), do: user |> Map.put(:roles, [Role.get_default()]) def handle_empty_user_roles(%User{} = user), do: user |> Map.put(:roles, [Role.get_default()]) @@ -93,7 +150,12 @@ defmodule EpochtalkServer.Models.Role do The `banned` `Role` takes priority over all other roles If a `User` is banned, only return the `banned` `Role` + This helper needs to be called anywhere that modifies a user's ban + and is expected to return the updated user's roles. """ + @spec handle_banned_user_role( + user_or_roles :: %EpochtalkServer.Models.User{} | [%EpochtalkServer.Models.Role{}] + ) :: %EpochtalkServer.Models.User{} | [%EpochtalkServer.Models.Role{}] # called with user model, outputs user model with updated role def handle_banned_user_role(%User{roles: [%Role{} | _] = roles} = user) do if banned_role = Enum.find(roles, &(&1.lookup == "banned")), @@ -103,6 +165,13 @@ defmodule EpochtalkServer.Models.Role do # called with just roles, ouput updated roles def handle_banned_user_role(roles), do: if ban_role = reduce_ban_role(roles), do: [ban_role], else: roles + + @doc """ + Takes in list of user's roles, and returns an xored map of all `Role` permissions + """ + @spec get_masked_permissions( + roles :: [%EpochtalkServer.Models.Role{}] + ) :: %{} # Given %User{}.roles, outputs xored permissions def get_masked_permissions(roles) when is_list(roles), do: Enum.reduce(roles, %{}, &mask_permissions(&2, &1)) From e3a34e177512d53ba35ef3c9a8f6d2746b9955b5 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Fri, 7 Oct 2022 16:32:35 -1000 Subject: [PATCH 201/231] docs(user): add docs to user model --- lib/epochtalk_server/models/user.ex | 91 ++++++++++++++++++++++++++--- 1 file changed, 83 insertions(+), 8 deletions(-) diff --git a/lib/epochtalk_server/models/user.ex b/lib/epochtalk_server/models/user.ex index a748c123..566a8fa3 100644 --- a/lib/epochtalk_server/models/user.ex +++ b/lib/epochtalk_server/models/user.ex @@ -11,6 +11,9 @@ defmodule EpochtalkServer.Models.User do alias EpochtalkServer.Models.BannedAddress alias EpochtalkServer.Models.BoardModerator alias EpochtalkServer.Models.Ban + @moduledoc """ + `User` model, for performing actions relating a user + """ schema "users" do field :email, :string @@ -38,6 +41,14 @@ defmodule EpochtalkServer.Models.User do ## === Changesets Functions === + @doc """ + Creates a registration changeset for `User` model, returns an error changeset + if validation of username, email and password do not pass. + """ + @spec registration_changeset( + user :: %EpochtalkServer.Models.User{}, + attrs :: %{} | nil + ) :: %EpochtalkServer.Models.User{} def registration_changeset(user, attrs) do user |> cast(attrs, [:id, :email, :username, :created_at, :updated_at, :deleted, :malicious_score, :password]) @@ -49,13 +60,12 @@ defmodule EpochtalkServer.Models.User do ## === Database Functions === - # create admin, for seeding - def create(attrs, true = _admin) do - Repo.transaction(fn -> - {:ok, user} = create(attrs) - RoleUser.set_admin(user.id) - end) - end + @doc """ + Creates a new `User` in the database, used for registration + """ + @spec create( + attrs :: %{} + ) :: {:ok, user :: %EpochtalkServer.Models.User{}} | {:error, Ecto.Changeset.t()} def create(attrs) do user_cs = User.registration_changeset(%User{}, attrs) case Repo.insert(user_cs) do @@ -68,14 +78,60 @@ defmodule EpochtalkServer.Models.User do end end + @doc """ + Creates a new `User` in the database and assigns the `superAdministrator` `Role`, used for seeding + """ + @spec create( + attrs :: %{}, + admin :: boolean + ) :: {:ok, user :: %EpochtalkServer.Models.User{}} | {:error, Ecto.Changeset.t()} + def create(attrs, true = _admin) do + Repo.transaction(fn -> + {:ok, user} = create(attrs) + RoleUser.set_admin(user.id) + end) + end + + @doc """ + Checks if `User` with `username` exists in the database + """ + @spec with_username_exists?( + username :: String.t() + ) :: true | false def with_username_exists?(username), do: Repo.exists?(from u in User, where: u.username == ^username) + @doc """ + Checks if `User` with `email` exists in the database + """ + @spec with_email_exists?( + email :: String.t() + ) :: true | false def with_email_exists?(email), do: Repo.exists?(from u in User, where: u.email == ^email) + @doc """ + Gets a `User` from the database by `id` + """ + @spec by_id( + id :: integer + ) :: %EpochtalkServer.Models.User{} | nil def by_id(id) when is_integer(id), do: Repo.get_by(User, id: id) + @doc """ + Clears the malicious score of a `User` by `id`, from the database + """ + @spec clear_malicious_score_by_id( + id :: integer + ) :: {non_neg_integer(), nil} def clear_malicious_score_by_id(id), do: set_malicious_score_by_id(id, nil) + @doc """ + Gets a `User` by `username`, from the database, with all of it's associations preloaded. + Appends the `user` `Role` to `user.roles` if no roles present. Strips all roles but + `banned` from `user.roles` if user is banned. + """ + @spec by_username( + username :: String.t() + ) :: {:ok, user :: %EpochtalkServer.Models.User{}} | {:error, :user_not_found} def by_username(username) when is_binary(username) do query = from u in User, where: u.username == ^username, @@ -85,6 +141,16 @@ defmodule EpochtalkServer.Models.User do else: {:error, :user_not_found} end + @doc """ + Checks if the provided `User` is malicious using the provided `ip`. If the `User` + is found to be malicious after checking `BannedAddress` records, the user's + `malicious_score` is updated and is assigned the `banned` `Role`, in the database + and in place. Otherwise the user is just returned with no change. + """ + @spec handle_malicious_user( + user :: %EpochtalkServer.Models.User{}, + ip :: tuple + ) :: {:ok, user :: %EpochtalkServer.Models.User{}} | {:error, :ban_error} def handle_malicious_user(%User{} = user, ip) do # convert ip tuple into string ip_str = ip |> :inet_parse.ntoa |> to_string @@ -98,7 +164,16 @@ defmodule EpochtalkServer.Models.User do else: Ban.ban(user) end - def valid_password?(%{passhash: hashed_password} = _user, password) + ## === Helper Functions === + + @doc """ + Validates with Argon2 that a `User` `passhash` matches the supplied `password` + """ + @spec valid_password?( + user :: %EpochtalkServer.Models.User{}, + password :: String.t() + ) :: true | false + def valid_password?(%User{passhash: hashed_password} = _user, password) when is_binary(hashed_password) and byte_size(password) > 0 do Argon2.verify_pass(password, hashed_password) end From ebe53c69f364ebd4e6676d087170159044d4cbc0 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 10 Oct 2022 12:21:26 -1000 Subject: [PATCH 202/231] docs(error-view): add docs and doctests to error view --- lib/epochtalk_server_web/views/error_view.ex | 21 ++++++++++++++++--- .../views/error_view_test.exs | 8 +++++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/lib/epochtalk_server_web/views/error_view.ex b/lib/epochtalk_server_web/views/error_view.ex index 4858f3e1..8c1282e5 100644 --- a/lib/epochtalk_server_web/views/error_view.ex +++ b/lib/epochtalk_server_web/views/error_view.ex @@ -1,10 +1,25 @@ defmodule EpochtalkServerWeb.ErrorView do use EpochtalkServerWeb, :view - # By default, Phoenix returns the status message from - # the template name. For example, "404.json" becomes - # "Not Found". + @doc """ + Render uses `template_not_found` when Phoenix cannot find the specified template, this + has been modified to handle and all types of errors. Epochtalk Server sends all errors + through this view to render a consistent error JSON. + ## Example + iex> EpochtalkServerWeb.ErrorView.render("500.json") + %{error: "Internal Server Error", message: "Request Error", status: 500} + iex> EpochtalkServerWeb.ErrorView.render("400.json") + %{error: "Bad Request", message: "Request Error", status: 400} + iex> EpochtalkServerWeb.ErrorView.render("404.json") + %{error: "Not Found", message: "Request Error", status: 404} + iex> EpochtalkServerWeb.ErrorView.render("401.json") + %{error: "Unauthorized", message: "Request Error", status: 401} + iex> EpochtalkServerWeb.ErrorView.template_not_found("DoesNotExist.json", %{message: "Custom Error Message", status: 500}) + %{error: "Internal Server Error", message: "Custom Error Message", status: 500} + iex> EpochtalkServerWeb.ErrorView.template_not_found("DoesNotExist.json", %{message: "Custom Error Message", status: 404}) + %{error: "Not Found", message: "Custom Error Message", status: 404} + """ def template_not_found(template, assigns), do: format_error(template, assigns) # match assigns.reason.message and assigns.conn.status diff --git a/test/epochtalk_server_web/views/error_view_test.exs b/test/epochtalk_server_web/views/error_view_test.exs index e87d799b..a6c4e9cc 100644 --- a/test/epochtalk_server_web/views/error_view_test.exs +++ b/test/epochtalk_server_web/views/error_view_test.exs @@ -3,9 +3,13 @@ defmodule EpochtalkServerWeb.ErrorViewTest do # Bring render/3 and render_to_string/3 for testing custom views import Phoenix.View + alias EpochtalkServerWeb.ErrorView + + # Specify that we want to use doctests: + doctest ErrorView test "renders 404.json" do - assert render(EpochtalkServerWeb.ErrorView, "404.json", []) == %{ + assert render(ErrorView, "404.json", []) == %{ error: "Not Found", message: "Request Error", status: 404 @@ -13,7 +17,7 @@ defmodule EpochtalkServerWeb.ErrorViewTest do end test "renders 500.json" do - assert render(EpochtalkServerWeb.ErrorView, "500.json", []) == %{ + assert render(ErrorView, "500.json", []) == %{ error: "Internal Server Error", message: "Request Error", status: 500 From 0414498b2141afb07d87edf6893bd21cb9a1cf57 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 10 Oct 2022 13:56:15 -1000 Subject: [PATCH 203/231] docs(auth-view): add docs and doc test to auth view --- lib/epochtalk_server_web/views/auth_view.ex | 25 +- .../views/auth_view_test.exs | 288 ++++++++++++++++++ 2 files changed, 312 insertions(+), 1 deletion(-) create mode 100644 test/epochtalk_server_web/views/auth_view_test.exs diff --git a/lib/epochtalk_server_web/views/auth_view.ex b/lib/epochtalk_server_web/views/auth_view.ex index c48740d4..150cc52a 100644 --- a/lib/epochtalk_server_web/views/auth_view.ex +++ b/lib/epochtalk_server_web/views/auth_view.ex @@ -2,11 +2,34 @@ defmodule EpochtalkServerWeb.AuthView do use EpochtalkServerWeb, :view alias EpochtalkServer.Models.Role alias EpochtalkServer.Models.User + @moduledoc """ + Renders `User` data for auth, in JSON format for frontend + """ + @doc """ + Renders if record was found, in JSON + + ## Example + iex> EpochtalkServerWeb.AuthView.render("search.json", %{found: true}) + %{found: true} + """ def render("search.json", %{found: found}), do: %{found: found} - def render("logout.json", _data), do: true + @doc """ + Renders if logout success, in JSON + + ## Example + iex> EpochtalkServerWeb.AuthView.render("logout.json", %{}) + %{success: true} + """ + def render("logout.json", _data), do: %{success: true} + + @doc """ + Renders formatted user JSON. Takes in a `User` with all associations preloaded + and outputs formatted user json used for auth. Masks all user's roles to generate + correct permissions set. + """ def render("user.json", %{user: user, token: token}), do: format_user_reply(user, token) # Format reply - from Models.User (login, register) diff --git a/test/epochtalk_server_web/views/auth_view_test.exs b/test/epochtalk_server_web/views/auth_view_test.exs new file mode 100644 index 00000000..9d0375fe --- /dev/null +++ b/test/epochtalk_server_web/views/auth_view_test.exs @@ -0,0 +1,288 @@ +defmodule EpochtalkServerWeb.AuthViewTest do + use EpochtalkServerWeb.ConnCase, async: true + # Bring render/3 and render_to_string/3 for testing custom views + import Phoenix.View + alias EpochtalkServerWeb.AuthView + alias EpochtalkServer.Models.User + alias EpochtalkServer.Models.Preference + alias EpochtalkServer.Models.Profile + alias EpochtalkServer.Models.Ban + alias EpochtalkServer.Models.Role + alias EpochtalkServer.Models.BoardModerator + + # Specify that we want to use doctests: + doctest EpochtalkServerWeb.AuthView + + # this was too long to be a doctest, we still need to test with Sandbox db connection + test "renders user.json" do + user = %User{ + id: 1, + email: "test@example.com", + username: "Test", + passhash: "************", + confirmation_token: nil, + reset_token: nil, + reset_expiration: nil, + created_at: nil, + imported_at: nil, + updated_at: nil, + deleted: false, + malicious_score: 1.0416, + smf_member: nil, + preferences: %Preference{ + user_id: 1, + posts_per_page: 25, + threads_per_page: 25, + collapsed_categories: %{"cats" => []}, + ignored_boards: %{"boards" => []}, + timezone_offset: "", + notify_replied_threads: true, + ignore_newbies: true, + patroller_view: false, + email_mentions: true, + email_messages: true + }, + profile: %Profile{ + id: 1, + user_id: 1, + avatar: "", + position: nil, + signature: "-my signature", + raw_signature: nil, + post_count: 1, + fields: nil, + last_active: nil + }, + ban_info: %Ban{ + id: 1, + user_id: 1, + expiration: ~N[2022-10-01 06:21:52], + created_at: ~N[2022-09-30 00:09:06], + updated_at: ~N[2022-10-01 06:21:52] + }, + roles: [ + %Role{ + id: 5, + name: "User", + description: "Standard account with access to create threads and post", + lookup: "user", + priority: 4, + highlight_color: nil, + permissions: %{ + "ads" => %{ + "analyticsView" => %{"allow" => true}, + "roundInfo" => %{"allow" => true}, + "view" => %{"allow" => true} + }, + "boards" => %{ + "allCategories" => %{"allow" => true}, + "find" => %{"allow" => true} + }, + "conversations" => %{ + "create" => %{"allow" => true}, + "delete" => %{"allow" => true}, + "messages" => %{"allow" => true} + }, + "invitations" => %{"invite" => %{"allow" => true}}, + "mentions" => %{ + "create" => %{"allow" => true}, + "delete" => %{"allow" => true}, + "page" => %{"allow" => true} + }, + "messages" => %{ + "create" => %{"allow" => true}, + "delete" => %{"allow" => true, "bypass" => %{"owner" => true}}, + "latest" => %{"allow" => true} + }, + "motd" => %{"get" => %{"allow" => true}}, + "notifications" => %{ + "counts" => %{"allow" => true}, + "dismiss" => %{"allow" => true} + }, + "portal" => %{"view" => %{"allow" => true}}, + "posts" => %{ + "byThread" => %{ + "allow" => true, + "bypass" => %{"viewDeletedPosts" => %{"selfMod" => true}} + }, + "create" => %{"allow" => true}, + "delete" => %{ + "allow" => true, + "bypass" => %{ + "locked" => %{"selfMod" => true}, + "owner" => %{"selfMod" => true} + } + }, + "find" => %{"allow" => true}, + "lock" => %{ + "allow" => true, + "bypass" => %{"lock" => %{"selfMod" => true}} + }, + "pageByUser" => %{"allow" => true}, + "search" => %{"allow" => true}, + "update" => %{"allow" => true} + }, + "reports" => %{ + "createMessageReport" => %{"allow" => true}, + "createPostReport" => %{"allow" => true}, + "createUserReport" => %{"allow" => true} + }, + "threads" => %{ + "byBoard" => %{"allow" => true}, + "create" => %{"allow" => true}, + "createPoll" => %{"allow" => true}, + "editPoll" => %{"allow" => true}, + "lockPoll" => %{"allow" => true}, + "moderated" => %{"allow" => true}, + "posted" => %{"allow" => true}, + "removeVote" => %{"allow" => true}, + "title" => %{"allow" => true}, + "viewed" => %{"allow" => true}, + "vote" => %{"allow" => true} + }, + "userTrust" => %{"addTrustFeedback" => %{"allow" => true}}, + "users" => %{ + "deactivate" => %{"allow" => true}, + "find" => %{"allow" => true}, + "lookup" => %{"allow" => true}, + "pagePublic" => %{"allow" => true}, + "reactivate" => %{"allow" => true}, + "update" => %{"allow" => true} + }, + "watchlist" => %{ + "edit" => %{"allow" => true}, + "pageBoards" => %{"allow" => true}, + "pageThreads" => %{"allow" => true}, + "unread" => %{"allow" => true}, + "unwatchBoard" => %{"allow" => true}, + "unwatchThread" => %{"allow" => true}, + "watchBoard" => %{"allow" => true}, + "watchThread" => %{"allow" => true} + } + }, + priority_restrictions: nil, + created_at: ~N[2022-08-30 00:45:30], + updated_at: ~N[2022-08-30 00:45:30] + } + ], + moderating: [ + %BoardModerator{ + user_id: 1, + board_id: 1, + }, + %BoardModerator{ + user_id: 1, + board_id: 2, + } + ] + } + token = "********" + assert render(AuthView, "user.json", %{user: user, token: token}) == %{ + avatar: "", + ban_expiration: ~N[2022-10-01 06:21:52], + id: 1, + malicious_score: 1.0416, + moderating: [1, 2], + permissions: %{ + highlight_color: nil, + permissions: %{ + "ads" => %{ + "analyticsView" => %{"allow" => true}, + "roundInfo" => %{"allow" => true}, + "view" => %{"allow" => true} + }, + "boards" => %{ + "allCategories" => %{"allow" => true}, + "find" => %{"allow" => true} + }, + "conversations" => %{ + "create" => %{"allow" => true}, + "delete" => %{"allow" => true}, + "messages" => %{"allow" => true} + }, + "invitations" => %{"invite" => %{"allow" => true}}, + "mentions" => %{ + "create" => %{"allow" => true}, + "delete" => %{"allow" => true}, + "page" => %{"allow" => true} + }, + "messages" => %{ + "create" => %{"allow" => true}, + "delete" => %{"allow" => true, "bypass" => %{"owner" => true}}, + "latest" => %{"allow" => true} + }, + "motd" => %{"get" => %{"allow" => true}}, + "notifications" => %{ + "counts" => %{"allow" => true}, + "dismiss" => %{"allow" => true} + }, + "portal" => %{"view" => %{"allow" => true}}, + "posts" => %{ + "byThread" => %{ + "allow" => true, + "bypass" => %{"viewDeletedPosts" => %{"selfMod" => true}} + }, + "create" => %{"allow" => true}, + "delete" => %{ + "allow" => true, + "bypass" => %{ + "locked" => %{"selfMod" => true}, + "owner" => %{"selfMod" => true} + } + }, + "find" => %{"allow" => true}, + "lock" => %{ + "allow" => true, + "bypass" => %{"lock" => %{"selfMod" => true}} + }, + "pageByUser" => %{"allow" => true}, + "search" => %{"allow" => true}, + "update" => %{"allow" => true} + }, + "reports" => %{ + "createMessageReport" => %{"allow" => true}, + "createPostReport" => %{"allow" => true}, + "createUserReport" => %{"allow" => true} + }, + "threads" => %{ + "byBoard" => %{"allow" => true}, + "create" => %{"allow" => true}, + "createPoll" => %{"allow" => true}, + "editPoll" => %{"allow" => true}, + "lockPoll" => %{"allow" => true}, + "moderated" => %{"allow" => true}, + "posted" => %{"allow" => true}, + "removeVote" => %{"allow" => true}, + "title" => %{"allow" => true}, + "viewed" => %{"allow" => true}, + "vote" => %{"allow" => true} + }, + "userTrust" => %{"addTrustFeedback" => %{"allow" => true}}, + "users" => %{ + "deactivate" => %{"allow" => true}, + "find" => %{"allow" => true}, + "lookup" => %{"allow" => true}, + "pagePublic" => %{"allow" => true}, + "reactivate" => %{"allow" => true}, + "update" => %{"allow" => true} + }, + "watchlist" => %{ + "edit" => %{"allow" => true}, + "pageBoards" => %{"allow" => true}, + "pageThreads" => %{"allow" => true}, + "unread" => %{"allow" => true}, + "unwatchBoard" => %{"allow" => true}, + "unwatchThread" => %{"allow" => true}, + "watchBoard" => %{"allow" => true}, + "watchThread" => %{"allow" => true} + } + }, + priority: 4, + priority_restrictions: nil + }, + roles: ["user"], + token: "********", + username: "Test" + } + end +end From 2ff1eb95d346f38cda953f81ec3c764872f921ef Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 10 Oct 2022 15:05:34 -1000 Subject: [PATCH 204/231] refactor(auth): renamed auth -> user, added docs/tests --- ...{auth_controller.ex => user_controller.ex} | 10 +++--- lib/epochtalk_server_web/router.ex | 12 +++---- lib/epochtalk_server_web/views/error_view.ex | 24 ++++++------- .../views/{auth_view.ex => user_view.ex} | 35 ++++++++----------- ...{auth_view_test.exs => user_view_test.exs} | 8 ++--- 5 files changed, 41 insertions(+), 48 deletions(-) rename lib/epochtalk_server_web/controllers/{auth_controller.ex => user_controller.ex} (92%) rename lib/epochtalk_server_web/views/{auth_view.ex => user_view.ex} (78%) rename test/epochtalk_server_web/views/{auth_view_test.exs => user_view_test.exs} (98%) diff --git a/lib/epochtalk_server_web/controllers/auth_controller.ex b/lib/epochtalk_server_web/controllers/user_controller.ex similarity index 92% rename from lib/epochtalk_server_web/controllers/auth_controller.ex rename to lib/epochtalk_server_web/controllers/user_controller.ex index 8abfb92d..0c02dafb 100644 --- a/lib/epochtalk_server_web/controllers/auth_controller.ex +++ b/lib/epochtalk_server_web/controllers/user_controller.ex @@ -1,4 +1,4 @@ -defmodule EpochtalkServerWeb.AuthController do +defmodule EpochtalkServerWeb.UserController do use EpochtalkServerWeb, :controller alias EpochtalkServer.Models.User alias EpochtalkServer.Models.Ban @@ -14,9 +14,9 @@ defmodule EpochtalkServerWeb.AuthController do InvalidPayload } - def username(conn, %{"username" => username}), do: render(conn, "search.json", found: User.with_username_exists?(username)) + def username(conn, %{"username" => username}), do: render(conn, "username.json", data: %{found: User.with_username_exists?(username)}) - def email(conn, %{"email" => email}), do: render(conn, "search.json", found: User.with_email_exists?(email)) + def email(conn, %{"email" => email}), do: render(conn, "email.json", data: %{found: User.with_email_exists?(email)}) # TODO(akinsey): handle config.inviteOnly # TODO(akinsey): handle config.newbieEnabled @@ -52,7 +52,7 @@ defmodule EpochtalkServerWeb.AuthController do def logout(conn, _attrs) do with {:auth, true} <- {:auth, Guardian.Plug.authenticated?(conn)}, conn <- Guardian.Plug.sign_out(conn) do - render(conn, "logout.json") + render(conn, "logout.json", data: %{success: true}) else {:auth, false} -> raise(NotLoggedIn) _ -> ErrorHelpers.render_json_error(conn, 500, "There was an issue signing out") @@ -67,7 +67,7 @@ defmodule EpochtalkServerWeb.AuthController do {:missing_passhash, true} <- {:missing_passhash, !!Map.get(user, :passhash)}, {:valid_password, true} <- {:valid_password, User.valid_password?(user, password)}, {:ok, user} = Ban.unban(user), - {:ok, user, token, conn} = Session.create(user, remember_me, conn) do + {:ok, user, token, conn} = Session.create(user, remember_me, conn)do render(conn, "user.json", %{ user: user, token: token}) else {:auth, true} -> ErrorHelpers.render_json_error(conn, 400, "Already logged in") diff --git a/lib/epochtalk_server_web/router.ex b/lib/epochtalk_server_web/router.ex index ca95960b..6ea84c12 100644 --- a/lib/epochtalk_server_web/router.ex +++ b/lib/epochtalk_server_web/router.ex @@ -18,16 +18,16 @@ defmodule EpochtalkServerWeb.Router do scope "/api", EpochtalkServerWeb do pipe_through [:api, :maybe_auth, :enforce_auth] - get "/authenticate", AuthController, :authenticate + get "/authenticate", UserController, :authenticate end scope "/api", EpochtalkServerWeb do pipe_through [:api, :maybe_auth] - get "/register/username/:username", AuthController, :username - get "/register/email/:email", AuthController, :email - post "/register", AuthController, :register - post "/login", AuthController, :login - delete "/logout", AuthController, :logout + get "/register/username/:username", UserController, :username + get "/register/email/:email", UserController, :email + post "/register", UserController, :register + post "/login", UserController, :login + delete "/logout", UserController, :logout end # Enables the Swoosh mailbox preview in development. diff --git a/lib/epochtalk_server_web/views/error_view.ex b/lib/epochtalk_server_web/views/error_view.ex index 8c1282e5..027bd2b2 100644 --- a/lib/epochtalk_server_web/views/error_view.ex +++ b/lib/epochtalk_server_web/views/error_view.ex @@ -7,18 +7,18 @@ defmodule EpochtalkServerWeb.ErrorView do through this view to render a consistent error JSON. ## Example - iex> EpochtalkServerWeb.ErrorView.render("500.json") - %{error: "Internal Server Error", message: "Request Error", status: 500} - iex> EpochtalkServerWeb.ErrorView.render("400.json") - %{error: "Bad Request", message: "Request Error", status: 400} - iex> EpochtalkServerWeb.ErrorView.render("404.json") - %{error: "Not Found", message: "Request Error", status: 404} - iex> EpochtalkServerWeb.ErrorView.render("401.json") - %{error: "Unauthorized", message: "Request Error", status: 401} - iex> EpochtalkServerWeb.ErrorView.template_not_found("DoesNotExist.json", %{message: "Custom Error Message", status: 500}) - %{error: "Internal Server Error", message: "Custom Error Message", status: 500} - iex> EpochtalkServerWeb.ErrorView.template_not_found("DoesNotExist.json", %{message: "Custom Error Message", status: 404}) - %{error: "Not Found", message: "Custom Error Message", status: 404} + iex> EpochtalkServerWeb.ErrorView.render("500.json") + %{error: "Internal Server Error", message: "Request Error", status: 500} + iex> EpochtalkServerWeb.ErrorView.render("400.json") + %{error: "Bad Request", message: "Request Error", status: 400} + iex> EpochtalkServerWeb.ErrorView.render("404.json") + %{error: "Not Found", message: "Request Error", status: 404} + iex> EpochtalkServerWeb.ErrorView.render("401.json") + %{error: "Unauthorized", message: "Request Error", status: 401} + iex> EpochtalkServerWeb.ErrorView.template_not_found("DoesNotExist.json", %{message: "Custom Error Message", status: 500}) + %{error: "Internal Server Error", message: "Custom Error Message", status: 500} + iex> EpochtalkServerWeb.ErrorView.template_not_found("DoesNotExist.json", %{message: "Custom Error Message", status: 404}) + %{error: "Not Found", message: "Custom Error Message", status: 404} """ def template_not_found(template, assigns), do: format_error(template, assigns) diff --git a/lib/epochtalk_server_web/views/auth_view.ex b/lib/epochtalk_server_web/views/user_view.ex similarity index 78% rename from lib/epochtalk_server_web/views/auth_view.ex rename to lib/epochtalk_server_web/views/user_view.ex index 150cc52a..c196f73b 100644 --- a/lib/epochtalk_server_web/views/auth_view.ex +++ b/lib/epochtalk_server_web/views/user_view.ex @@ -1,30 +1,11 @@ -defmodule EpochtalkServerWeb.AuthView do +defmodule EpochtalkServerWeb.UserView do use EpochtalkServerWeb, :view alias EpochtalkServer.Models.Role alias EpochtalkServer.Models.User @moduledoc """ - Renders `User` data for auth, in JSON format for frontend + Renders and formats `User` data, in JSON format for frontend """ - @doc """ - Renders if record was found, in JSON - - ## Example - iex> EpochtalkServerWeb.AuthView.render("search.json", %{found: true}) - %{found: true} - """ - def render("search.json", %{found: found}), do: %{found: found} - - @doc """ - Renders if logout success, in JSON - - ## Example - iex> EpochtalkServerWeb.AuthView.render("logout.json", %{}) - %{success: true} - """ - def render("logout.json", _data), do: %{success: true} - - @doc """ Renders formatted user JSON. Takes in a `User` with all associations preloaded and outputs formatted user json used for auth. Masks all user's roles to generate @@ -32,6 +13,18 @@ defmodule EpochtalkServerWeb.AuthView do """ def render("user.json", %{user: user, token: token}), do: format_user_reply(user, token) + @doc """ + Renders whatever data it is passed when template not found. Data pass through + for rendering misc responses (ex: {found: true} or {success: true}) + ## Example + iex> EpochtalkServerWeb.UserView.render("DoesNotExist.json", data: %{found: true}) + %{found: true} + iex> EpochtalkServerWeb.UserView.render("DoesNotExist.json", data: %{success: true}) + %{success: true} + """ + def template_not_found(_template, %{conn: %{ assigns: %{ data: data }}}), do: data + def template_not_found(_template, %{ data: data }), do: data + # Format reply - from Models.User (login, register) defp format_user_reply(%User{} = user, token) do avatar = if user.profile, do: user.profile.avatar, else: nil diff --git a/test/epochtalk_server_web/views/auth_view_test.exs b/test/epochtalk_server_web/views/user_view_test.exs similarity index 98% rename from test/epochtalk_server_web/views/auth_view_test.exs rename to test/epochtalk_server_web/views/user_view_test.exs index 9d0375fe..775d65f6 100644 --- a/test/epochtalk_server_web/views/auth_view_test.exs +++ b/test/epochtalk_server_web/views/user_view_test.exs @@ -1,8 +1,8 @@ -defmodule EpochtalkServerWeb.AuthViewTest do +defmodule EpochtalkServerWeb.UserViewTest do use EpochtalkServerWeb.ConnCase, async: true # Bring render/3 and render_to_string/3 for testing custom views import Phoenix.View - alias EpochtalkServerWeb.AuthView + alias EpochtalkServerWeb.UserView alias EpochtalkServer.Models.User alias EpochtalkServer.Models.Preference alias EpochtalkServer.Models.Profile @@ -11,7 +11,7 @@ defmodule EpochtalkServerWeb.AuthViewTest do alias EpochtalkServer.Models.BoardModerator # Specify that we want to use doctests: - doctest EpochtalkServerWeb.AuthView + doctest EpochtalkServerWeb.UserView # this was too long to be a doctest, we still need to test with Sandbox db connection test "renders user.json" do @@ -177,7 +177,7 @@ defmodule EpochtalkServerWeb.AuthViewTest do ] } token = "********" - assert render(AuthView, "user.json", %{user: user, token: token}) == %{ + assert render(UserView, "user.json", %{user: user, token: token}) == %{ avatar: "", ban_expiration: ~N[2022-10-01 06:21:52], id: 1, From 48ddd4ffae58c37129ed42396ee3baef5c734c97 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 11 Oct 2022 12:52:12 -1000 Subject: [PATCH 205/231] refactor(error-handling): modify perpare parse to raise error, cleans up console output --- .../errors/custom_errors.ex | 16 ++++++++++- .../plugs/prepare_parse.ex | 28 ++++++++++--------- lib/epochtalk_server_web/views/error_view.ex | 3 ++ 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/lib/epochtalk_server_web/errors/custom_errors.ex b/lib/epochtalk_server_web/errors/custom_errors.ex index b62cfa49..8806ccd8 100644 --- a/lib/epochtalk_server_web/errors/custom_errors.ex +++ b/lib/epochtalk_server_web/errors/custom_errors.ex @@ -29,11 +29,25 @@ defmodule EpochtalkServerWeb.CustomErrors do defexception plug_status: 403, message: "User account migration not complete, please reset password", conn: nil, router: nil end - # API parameter mismatch handling + # API Payload Handling defmodule InvalidPayload do @moduledoc """ Exception raised when api request payload is incorrect """ defexception plug_status: 400, message: "Invalid payload, check that the request payload is correct", conn: nil, router: nil end + + defmodule MalformedPayload do + @moduledoc """ + Exception raised when api request payload JSON is malformed + """ + defexception plug_status: 400, message: "Malformed JSON in request body", conn: nil, router: nil + end + + defmodule OversizedPayload do + @moduledoc """ + Exception raised when api request payload is too large + """ + defexception plug_status: 400, message: "Payload too large", conn: nil, router: nil + end end diff --git a/lib/epochtalk_server_web/plugs/prepare_parse.ex b/lib/epochtalk_server_web/plugs/prepare_parse.ex index ab2091be..8f62a450 100644 --- a/lib/epochtalk_server_web/plugs/prepare_parse.ex +++ b/lib/epochtalk_server_web/plugs/prepare_parse.ex @@ -1,34 +1,36 @@ defmodule EpochtalkServerWeb.Plugs.PrepareParse do - import Plug.Conn - use Phoenix.Controller - alias EpochtalkServerWeb.ErrorView + @moduledoc """ + Plug that pre-parses request body and raises errors if there are problems + """ + alias EpochtalkServerWeb.CustomErrors.{ + MalformedPayload, + OversizedPayload + } @env Application.compile_env(:application_api, :env) @methods ~w(POST PUT PATCH) + @doc """ + default Plug init function + """ def init(opts), do: opts + @doc """ + Pre-parses request body and checks for errors with payload. (ex: Malformed JSON or Payload too large) + """ def call(conn, opts) do %{method: method} = conn - if method in @methods and not (@env in [:test]) do case Plug.Conn.read_body(conn, opts) do {:error, :timeout} -> raise Plug.TimeoutError {:error, _} -> raise Plug.BadRequestError - {:more, _, conn} -> render_error(conn, %{message: "Payload too large error"}) + {:more, _, _} -> raise OversizedPayload {:ok, "" = body, conn} -> update_in(conn.assigns[:raw_body], &[body | &1 || []]) {:ok, body, conn} -> case Jason.decode(body) do {:ok, _result} -> update_in(conn.assigns[:raw_body], &[body | &1 || []]) - {:error, _reason} -> render_error(conn, %{message: "Malformed JSON in the body"}) + {:error, _reason} -> raise MalformedPayload end end else conn end end - - def render_error(conn, %{ message: message } = _error) do - conn - |> put_status(400) - |> put_view(ErrorView) - |> render("400.json", message: message) - end end diff --git a/lib/epochtalk_server_web/views/error_view.ex b/lib/epochtalk_server_web/views/error_view.ex index 027bd2b2..8f87b68f 100644 --- a/lib/epochtalk_server_web/views/error_view.ex +++ b/lib/epochtalk_server_web/views/error_view.ex @@ -1,4 +1,7 @@ defmodule EpochtalkServerWeb.ErrorView do + @moduledoc """ + Renders and formats error data, in JSON format for frontend + """ use EpochtalkServerWeb, :view @doc """ From 7e59bdc47d64a909393442ce5e55a5344e9dd6f5 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 11 Oct 2022 12:52:56 -1000 Subject: [PATCH 206/231] docs(user-controller): add docs to user controller --- .../controllers/user_controller.ex | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/lib/epochtalk_server_web/controllers/user_controller.ex b/lib/epochtalk_server_web/controllers/user_controller.ex index 0c02dafb..dc6d4477 100644 --- a/lib/epochtalk_server_web/controllers/user_controller.ex +++ b/lib/epochtalk_server_web/controllers/user_controller.ex @@ -1,5 +1,8 @@ defmodule EpochtalkServerWeb.UserController do use EpochtalkServerWeb, :controller + @moduledoc """ + Controller For `User` related API requests + """ alias EpochtalkServer.Models.User alias EpochtalkServer.Models.Ban alias EpochtalkServer.Models.Invitation @@ -14,13 +17,23 @@ defmodule EpochtalkServerWeb.UserController do InvalidPayload } + @doc """ + Used to check if a username has already been taken + """ def username(conn, %{"username" => username}), do: render(conn, "username.json", data: %{found: User.with_username_exists?(username)}) + @doc """ + Used to check if an email has already been taken + """ def email(conn, %{"email" => email}), do: render(conn, "email.json", data: %{found: User.with_email_exists?(email)}) - # TODO(akinsey): handle config.inviteOnly - # TODO(akinsey): handle config.newbieEnabled - # TODO(akinsey): Send confirmation email + @doc """ + Registers a new `User` + + - TODO(akinsey): handle config.inviteOnly + - TODO(akinsey): handle config.newbieEnabled + - TODO(akinsey): Send confirmation email + """ def register(conn, %{"username" => _, "email" => _, "password" => _} = attrs) do with {:auth, false} <- {:auth, Guardian.Plug.authenticated?(conn)}, # check auth {:ok, user} <- User.create(attrs), # create user @@ -41,14 +54,21 @@ defmodule EpochtalkServerWeb.UserController do end def register(_conn, _attrs), do: raise(InvalidPayload) - def authenticate(conn), do: authenticate(conn, nil) + @doc """ + Authenticates currently logged in `User` + """ def authenticate(conn, _attrs) do token = Guardian.Plug.current_token(conn) user = Guardian.Plug.current_resource(conn) render(conn, "user.json", %{ user: user, token: token}) end - # TODO: check if user is on page that requires auth + @doc """ + Logs out the logged in `User` + + - TODO(boka): check if user is on page that requires auth + - TODO(boka): Delete users session + """ def logout(conn, _attrs) do with {:auth, true} <- {:auth, Guardian.Plug.authenticated?(conn)}, conn <- Guardian.Plug.sign_out(conn) do @@ -59,6 +79,9 @@ defmodule EpochtalkServerWeb.UserController do end end + @doc """ + Logs in an existing `User` + """ def login(conn, attrs) when not is_map_key(attrs, "rememberMe"), do: login(conn, Map.put(attrs, "rememberMe", false)) def login(conn, %{"username" => username, "password" => password, "rememberMe" => remember_me} = _attrs) do with {:auth, false} <- {:auth, Guardian.Plug.authenticated?(conn)}, From 7cd039b1f25c9688a6976dce4a871a6dfa34787c Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 11 Oct 2022 12:54:26 -1000 Subject: [PATCH 207/231] docs(session): add docs to session code --- lib/epochtalk_server/session.ex | 98 +++++++++++++++++++++++---------- 1 file changed, 70 insertions(+), 28 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index b2c3b115..a9e14124 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -1,11 +1,24 @@ defmodule EpochtalkServer.Session do + @moduledoc """ + Manages `User` sessions in Redis. Used by Auth related `User` actions. + """ alias EpochtalkServer.Auth.Guardian alias EpochtalkServer.Models.User - # set user session id, timestamp, ttl - # log user in with Guardian to get token - # save user session info to redis - # returns {:ok, user, token and conn} + @doc """ + Create session performs the following actions: + * Sets user's session id, timestamp, ttl + * Logs `User` in with Guardian to get token + * Saves `User` session info to redis (avatar, roles, moderating, ban info, etc) + * returns {:ok, user, token and conn} + + - TODO(boka): Handle expiration of redis sessions (this is handled in guardian but not redis) + """ + @spec create( + user :: %EpochtalkServer.Models.User{}, + remember_me :: boolean, + conn :: Plug.Conn.t() + ) :: {:ok, user :: %EpochtalkServer.Models.User{}, encoded_token :: String.t(), conn :: Plug.Conn.t()} def create(%User{} = user, remember_me, conn) do datetime = NaiveDateTime.utc_now session_id = UUID.uuid1() @@ -24,6 +37,47 @@ defmodule EpochtalkServer.Session do # return user with token {:ok, user, encoded_token, conn} end + + @doc """ + Gets all sessions for a specific `User` + """ + @spec get_sessions( + user_id :: integer + ) :: {:ok, Redix.Protocol.redis_value()} | {:error, atom() | Redix.Error.t() | Redix.ConnectionError.t()} + def get_sessions(user_id) do + # get session id's from redis under "user:{user_id}:sessions" + session_key = generate_key(user_id, "sessions") + Redix.command(:redix, ["SMEMBERS", session_key]) + end + + @doc """ + Deletes a specific `User` session + """ + @spec delete_session( + user_id :: integer, + session_id :: String.t() + ) :: {:ok, Redix.Protocol.redis_value()} | {:error, atom() | Redix.Error.t() | Redix.ConnectionError.t()} + def delete_session(user_id, session_id) do + # delete session id from redis under "user:{user_id}:sessions" + session_key = generate_key(user_id, "sessions") + Redix.command(:redix, ["SREM", session_key, session_id]) + end + + @doc """ + Deletes every session instance for the specified `User` + """ + @spec delete_sessions( + user_id :: integer, + session_id :: String.t() + ) :: {:ok, Redix.Protocol.redis_value()} | {:error, atom() | Redix.Error.t() | Redix.ConnectionError.t()} + def delete_sessions(user_id, session_id) do + # delete session id from redis under "user:{user_id}:sessions" + session_key = generate_key(user_id, "sessions") + Redix.command(:redix, ["SPOP", session_key, session_id]) + # repeat until redix returns nil + |> unless do delete_sessions(user_id, session_id) end + end + defp save(%User{} = user, session_id) do # TODO: return role lookups from db instead of entire roles avatar = if is_nil(user.profile), do: nil, else: user.profile.avatar @@ -35,8 +89,9 @@ defmodule EpochtalkServer.Session do update_moderating(user.id, user.moderating) set_session(user.id, session_id) end + # use default role - def update_roles(user_id, roles) when is_list(roles) do + defp update_roles(user_id, roles) when is_list(roles) do # save/replace roles to redis under "user:{user_id}:roles" role_lookups = roles |> Enum.map(&(&1.lookup)) role_key = generate_key(user_id, "roles") @@ -44,7 +99,8 @@ defmodule EpochtalkServer.Session do unless role_lookups == nil or role_lookups == [], do: Enum.each(role_lookups, &Redix.command(:redix, ["SADD", role_key, &1])) end - def update_moderating(user_id, moderating) do + + defp update_moderating(user_id, moderating) do # get list of board ids from user.moderating moderating = moderating |> Enum.map(&(&1.board_id)) # save/replace moderating boards to redis under "user:{user_id}:moderating" @@ -53,51 +109,37 @@ defmodule EpochtalkServer.Session do unless moderating == nil or moderating == [], do: Enum.each(moderating, &Redix.command(:redix, ["SADD", moderating_key, &1])) end - def update_user_info(user_id, username) do + + defp update_user_info(user_id, username) do user_key = generate_key(user_id, "user") # delete avatar from redis hash under "user:{user_id}" Redix.command(:redix, ["HDEL", user_key, "avatar"]) # save username to redis hash under "user:{user_id}" Redix.command(:redix, ["HSET", user_key, "username", username]) end - def update_user_info(user_id, username, avatar) when is_nil(avatar) or avatar == "" do + defp update_user_info(user_id, username, avatar) when is_nil(avatar) or avatar == "" do update_user_info(user_id, username) end - def update_user_info(user_id, username, avatar) do + defp update_user_info(user_id, username, avatar) do # save username, avatar to redis hash under "user:{user_id}" user_key = generate_key(user_id, "user") Redix.command(:redix, ["HSET", user_key, "username", username, "avatar", avatar]) end - def update_ban_info(user_id, ban_info) do + + defp update_ban_info(user_id, ban_info) do # save/replace ban_expiration to redis under "user:{user_id}:baninfo" ban_key = generate_key(user_id, "baninfo") Redix.command(:redix, ["HDEL", ban_key, "ban_expiration", "malicious_score"]) if ban_exp = Map.get(ban_info, :ban_expiration), do: Redix.command(:redix, ["HSET", ban_key, "ban_expiration", ban_exp]) if malicious_score = Map.get(ban_info, :malicious_score), do: Redix.command(:redix, ["HSET", ban_key, "malicious_score", malicious_score]) end + defp set_session(user_id, session_id) do # save session id to redis under "user:{user_id}:sessions" session_key = generate_key(user_id, "sessions") Redix.command(:redix, ["SADD", session_key, session_id]) end - defp get_sessions(user_id) do - # get session id's from redis under "user:{user_id}:sessions" - session_key = generate_key(user_id, "sessions") - Redix.command(:redix, ["SMEMBERS", session_key]) - end - # TODO: invalidate token - defp delete_session(user_id, session_id) do - # delete session id from redis under "user:{user_id}:sessions" - session_key = generate_key(user_id, "sessions") - Redix.command(:redix, ["SREM", session_key, session_id]) - end - defp delete_sessions(user_id, session_id) do - # delete session id from redis under "user:{user_id}:sessions" - session_key = generate_key(user_id, "sessions") - Redix.command(:redix, ["SPOP", session_key, session_id]) - # repeat until redix returns nil - |> unless do delete_sessions(user_id, session_id) end - end + defp generate_key(user_id, "user"), do: "user:#{user_id}" defp generate_key(user_id, type), do: "user:#{user_id}:#{type}" end From 645d23031766c154a789dcfbae6c020b48149bc9 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 11 Oct 2022 15:50:04 -1000 Subject: [PATCH 208/231] refactor(session): refactor to take in User, add error handling, update docs --- lib/epochtalk_server/session.ex | 46 +++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index a9e14124..ec189ff8 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -15,10 +15,10 @@ defmodule EpochtalkServer.Session do - TODO(boka): Handle expiration of redis sessions (this is handled in guardian but not redis) """ @spec create( - user :: %EpochtalkServer.Models.User{}, + user :: User.t(), remember_me :: boolean, conn :: Plug.Conn.t() - ) :: {:ok, user :: %EpochtalkServer.Models.User{}, encoded_token :: String.t(), conn :: Plug.Conn.t()} + ) :: {:ok, user :: User.t(), encoded_token :: String.t(), conn :: Plug.Conn.t()} def create(%User{} = user, remember_me, conn) do datetime = NaiveDateTime.utc_now session_id = UUID.uuid1() @@ -32,50 +32,56 @@ defmodule EpochtalkServer.Session do encoded_token = Guardian.Plug.current_token(conn) # save session - save(user, session_id) - # TODO(akinsey): error handling - # return user with token - {:ok, user, encoded_token, conn} + case save(user, session_id) do + {:ok, _} -> {:ok, user, encoded_token, conn} + {:error, error} -> {:error, error} + end end @doc """ Gets all sessions for a specific `User` """ @spec get_sessions( - user_id :: integer - ) :: {:ok, Redix.Protocol.redis_value()} | {:error, atom() | Redix.Error.t() | Redix.ConnectionError.t()} - def get_sessions(user_id) do + user :: User.t() + ) :: {:ok, user :: User.t(), sessions :: [String.t()]} | {:error, atom() | Redix.Error.t() | Redix.ConnectionError.t()} + def get_sessions(%User{} = user) do # get session id's from redis under "user:{user_id}:sessions" - session_key = generate_key(user_id, "sessions") - Redix.command(:redix, ["SMEMBERS", session_key]) + session_key = generate_key(user.id, "sessions") + case Redix.command(:redix, ["SMEMBERS", session_key]) do + {:ok, sessions} -> {:ok, user, sessions} + {:error, error} -> {:error, error} + end end @doc """ Deletes a specific `User` session """ @spec delete_session( - user_id :: integer, + user :: User.t(), session_id :: String.t() - ) :: {:ok, Redix.Protocol.redis_value()} | {:error, atom() | Redix.Error.t() | Redix.ConnectionError.t()} - def delete_session(user_id, session_id) do + ) :: {:ok, user :: User.t()} | {:error, atom() | Redix.Error.t() | Redix.ConnectionError.t()} + def delete_session(%User{} = user, session_id) do # delete session id from redis under "user:{user_id}:sessions" - session_key = generate_key(user_id, "sessions") - Redix.command(:redix, ["SREM", session_key, session_id]) + session_key = generate_key(user.id, "sessions") + case Redix.command(:redix, ["SREM", session_key, session_id]) do + {:ok, _} -> {:ok, user} + {:error, error} -> {:error, error} + end end @doc """ Deletes every session instance for the specified `User` """ @spec delete_sessions( - user_id :: integer, + user :: User.t(), session_id :: String.t() ) :: {:ok, Redix.Protocol.redis_value()} | {:error, atom() | Redix.Error.t() | Redix.ConnectionError.t()} - def delete_sessions(user_id, session_id) do + def delete_sessions(%User{} = user, session_id) do # delete session id from redis under "user:{user_id}:sessions" - session_key = generate_key(user_id, "sessions") + session_key = generate_key(user.id, "sessions") Redix.command(:redix, ["SPOP", session_key, session_id]) # repeat until redix returns nil - |> unless do delete_sessions(user_id, session_id) end + |> unless do delete_sessions(user.id, session_id) end end defp save(%User{} = user, session_id) do From f58c87bd34913fa0398b431526e0ebea020a6f3c Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 11 Oct 2022 15:53:57 -1000 Subject: [PATCH 209/231] docs(typespec): add type spec to all models, this cleans up the documentation in code and in the actual html docs quite a bit --- lib/epochtalk_server/models/ban.ex | 24 ++++++----- lib/epochtalk_server/models/banned_address.ex | 27 +++++++++---- lib/epochtalk_server/models/board.ex | 27 +++++++++---- lib/epochtalk_server/models/board_mapping.ex | 11 +++-- .../models/board_moderator.ex | 9 +++-- lib/epochtalk_server/models/category.ex | 28 ++++++++----- lib/epochtalk_server/models/invitation.ex | 11 +++-- lib/epochtalk_server/models/metadata_board.ex | 20 ++++++++-- lib/epochtalk_server/models/permission.ex | 11 +++-- lib/epochtalk_server/models/preference.ex | 18 +++++++-- lib/epochtalk_server/models/profile.ex | 15 +++++-- lib/epochtalk_server/models/role.ex | 40 ++++++++++++------- .../models/role_permission.ex | 14 +++++-- lib/epochtalk_server/models/role_user.ex | 13 ++++-- lib/epochtalk_server/models/user.ex | 40 ++++++++++++++----- 15 files changed, 219 insertions(+), 89 deletions(-) diff --git a/lib/epochtalk_server/models/ban.ex b/lib/epochtalk_server/models/ban.ex index 20d4a11b..ebe992e9 100644 --- a/lib/epochtalk_server/models/ban.ex +++ b/lib/epochtalk_server/models/ban.ex @@ -13,6 +13,12 @@ defmodule EpochtalkServer.Models.Ban do # max naivedatetime, used for permanent bans @max_date ~N[9999-12-31 00:00:00.000] + @type t :: %__MODULE__{ + user_id: non_neg_integer, + expiration: NaiveDateTime.t(), + created_at: NaiveDateTime.t(), + updated_at: NaiveDateTime.t() + } @schema_prefix "users" @derive {Jason.Encoder, only: [:user_id, :expiration, :created_at, :updated_at]} schema "bans" do @@ -28,9 +34,9 @@ defmodule EpochtalkServer.Models.Ban do Create generic changeset for `Ban` model """ @spec changeset( - ban :: %EpochtalkServer.Models.Ban{}, + ban :: t(), attrs :: %{} | nil - ) :: %EpochtalkServer.Models.Ban{} + ) :: t() def changeset(ban, attrs \\ %{}) do ban |> cast(attrs, [:id, :user_id, :expiration, :created_at, :updated_at]) @@ -41,9 +47,9 @@ defmodule EpochtalkServer.Models.Ban do Create ban changeset for `Ban` model, handles upsert of ban for banning """ @spec ban_changeset( - ban :: %EpochtalkServer.Models.Ban{}, + ban :: t(), attrs :: %{} | nil - ) :: %EpochtalkServer.Models.Ban{} + ) :: t() def ban_changeset(ban, attrs \\ %{}) do now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) attrs = attrs @@ -59,9 +65,9 @@ defmodule EpochtalkServer.Models.Ban do Create unban changeset for `Ban` model, handles update of ban for unbanning """ @spec unban_changeset( - ban :: %EpochtalkServer.Models.Ban{}, + ban :: t(), attrs :: %{} | nil - ) :: %EpochtalkServer.Models.Ban{} + ) :: t() def unban_changeset(ban, attrs \\ %{}) do now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) attrs = attrs @@ -112,7 +118,7 @@ defmodule EpochtalkServer.Models.Ban do Used to ban a `User` permanently. Updates supplied `User` model to reflect ban and returns. """ @spec ban( - user :: %EpochtalkServer.Models.User{} + user :: User.t() ) :: {:ok, user_changeset :: Ecto.Changeset.t()} | {:error, :ban_error} def ban(%User{} = user), do: ban(user, nil) @@ -121,7 +127,7 @@ defmodule EpochtalkServer.Models.Ban do permanently ban the `User`. Updates supplied `User` model to reflect ban and returns. """ @spec ban( - user :: %EpochtalkServer.Models.User{}, + user :: User.t(), expiration :: Calendar.naive_datetime() | nil ) :: {:ok, user_changeset :: Ecto.Changeset.t()} | {:error, :ban_error} def ban(%User{ id: id} = user, expiration) do @@ -162,7 +168,7 @@ defmodule EpochtalkServer.Models.Ban do Used to unban a `User`. Updates supplied `User` model to reflect unbanning and returns. """ @spec unban( - user :: %EpochtalkServer.Models.User{} + user :: User.t() ) :: {:ok, user_changeset :: Ecto.Changeset.t()} | {:error, :unban_error} def unban(%User{ban_info: %Ban{expiration: expiration}, id: user_id} = user) do if NaiveDateTime.compare(expiration, NaiveDateTime.utc_now) == :lt do diff --git a/lib/epochtalk_server/models/banned_address.ex b/lib/epochtalk_server/models/banned_address.ex index 7e5e1996..4b4a22ac 100644 --- a/lib/epochtalk_server/models/banned_address.ex +++ b/lib/epochtalk_server/models/banned_address.ex @@ -7,7 +7,18 @@ defmodule EpochtalkServer.Models.BannedAddress do @moduledoc """ `BannedAddress` model, for performing actions relating to banning by ip/hostname """ - + @type t :: %__MODULE__{ + hostname: String.t(), + ip1: non_neg_integer, + ip2: non_neg_integer, + ip3: non_neg_integer, + ip4: non_neg_integer, + weight: float, + decay: boolean, + created_at: NaiveDateTime.t(), + imported_at: NaiveDateTime.t(), + updates: [NaiveDateTime.t()] + } @primary_key false schema "banned_addresses" do field :hostname, :string @@ -28,9 +39,9 @@ defmodule EpochtalkServer.Models.BannedAddress do Creates changeset for upsert of `BannedAddress` model """ @spec upsert_changeset( - banned_address :: %EpochtalkServer.Models.BannedAddress{}, + banned_address :: t(), attrs :: %{} | nil - ) :: %EpochtalkServer.Models.BannedAddress{} + ) :: t() def upsert_changeset(banned_address, attrs \\ %{}) do now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) attrs = attrs @@ -53,9 +64,9 @@ defmodule EpochtalkServer.Models.BannedAddress do Creates changeset of `BannedAddress` model with hostname information """ @spec hostname_changeset( - banned_address :: %EpochtalkServer.Models.BannedAddress{}, + banned_address :: t(), attrs :: %{} | nil - ) :: %EpochtalkServer.Models.BannedAddress{} + ) :: t() def hostname_changeset(banned_address, attrs \\ %{}) do banned_address |> cast(attrs, [:hostname, :weight, :decay, :imported_at, :created_at, :updates]) @@ -67,9 +78,9 @@ defmodule EpochtalkServer.Models.BannedAddress do Creates changeset of `BannedAddress` model with IP information """ @spec ip_changeset( - banned_address :: %EpochtalkServer.Models.BannedAddress{}, + banned_address :: t(), attrs :: %{} | nil - ) :: %EpochtalkServer.Models.BannedAddress{} + ) :: t() def ip_changeset(banned_address, attrs \\ %{}) do cs_data = banned_address |> cast(attrs, [:ip1, :ip2, :ip3, :ip4, :weight, :decay, :imported_at, :created_at, :updates]) @@ -94,7 +105,7 @@ defmodule EpochtalkServer.Models.BannedAddress do Upserts a `BannedAddress` into the database and handles calculation of weight accounting for decay """ @spec upsert( - banned_address_or_list :: %{} | [%{}] | %EpochtalkServer.Models.BannedAddress{} | [%EpochtalkServer.Models.BannedAddress{}] + banned_address_or_list :: %{} | [%{}] | t() | [t()] ) :: {:ok, banned_address_changeset :: Ecto.Changeset.t()} | {:error, :banned_address_error} def upsert(address_list) when is_list(address_list) do Repo.transaction(fn -> Enum.each(address_list ,&upsert_one(&1)) end) diff --git a/lib/epochtalk_server/models/board.ex b/lib/epochtalk_server/models/board.ex index 425dead8..8a22865c 100644 --- a/lib/epochtalk_server/models/board.ex +++ b/lib/epochtalk_server/models/board.ex @@ -9,7 +9,20 @@ defmodule EpochtalkServer.Models.Board do @moduledoc """ `Board` model, for performing actions relating to forum boards """ - + @type t :: %__MODULE__{ + category: Category.t(), + name: String.t(), + slug: String.t(), + description: String.t(), + post_count: non_neg_integer, + thread_count: non_neg_integer, + viewable_by: non_neg_integer, + postable_by: non_neg_integer, + right_to_left: boolean, + created_at: NaiveDateTime.t(), + imported_at: NaiveDateTime.t(), + meta: %{} + } schema "boards" do field :name, :string field :slug, :string @@ -23,7 +36,7 @@ defmodule EpochtalkServer.Models.Board do field :imported_at, :naive_datetime field :updated_at, :naive_datetime field :meta, :map - many_to_many :categories, Category, join_through: BoardMapping + many_to_many :category, Category, join_through: BoardMapping end ## === Changesets Functions === @@ -32,9 +45,9 @@ defmodule EpochtalkServer.Models.Board do Create generic changeset for `Board` model """ @spec changeset( - board :: %EpochtalkServer.Models.Board{}, + board :: t(), attrs :: %{} | nil - ) :: %EpochtalkServer.Models.Board{} + ) :: t() def changeset(board, attrs) do board |> cast(attrs, [:id, :name, :slug, :description, :post_count, :thread_count, :viewable_by, :postable_by, :created_at, :imported_at, :updated_at, :meta]) @@ -49,9 +62,9 @@ defmodule EpochtalkServer.Models.Board do Create changeset for creation of `Board` model """ @spec create_changeset( - board :: %EpochtalkServer.Models.Board{}, + board :: t(), attrs :: %{} | nil - ) :: %EpochtalkServer.Models.Board{} + ) :: t() def create_changeset(board, attrs) do now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) attrs = attrs @@ -70,7 +83,7 @@ defmodule EpochtalkServer.Models.Board do """ @spec create( board_attrs :: %{} - ) :: {:ok, board :: %EpochtalkServer.Models.Board{}} | {:error, Ecto.Changeset.t()} + ) :: {:ok, board :: t()} | {:error, Ecto.Changeset.t()} def create(board) do board_cs = create_changeset(%Board{}, board) case Repo.insert(board_cs) do diff --git a/lib/epochtalk_server/models/board_mapping.ex b/lib/epochtalk_server/models/board_mapping.ex index 16829721..b2d79464 100644 --- a/lib/epochtalk_server/models/board_mapping.ex +++ b/lib/epochtalk_server/models/board_mapping.ex @@ -9,7 +9,12 @@ defmodule EpochtalkServer.Models.BoardMapping do @moduledoc """ `BoardMapping` model, for performing actions relating to mapping forum boards and categories """ - + @type t :: %__MODULE__{ + board: Board.t(), + parent: Board.t(), + category: Category.t(), + view_order: non_neg_integer + } @primary_key false schema "board_mapping" do belongs_to :board, Board, primary_key: true @@ -24,9 +29,9 @@ defmodule EpochtalkServer.Models.BoardMapping do Create generic changeset for `BoardMapping` model """ @spec changeset( - board_mapping :: %EpochtalkServer.Models.BoardMapping{}, + board_mapping :: t(), attrs :: %{} | nil - ) :: %EpochtalkServer.Models.BoardMapping{} + ) :: t() def changeset(board_mapping, attrs) do board_mapping |> cast(attrs, [:board_id, :parent_id, :category_id, :view_order]) diff --git a/lib/epochtalk_server/models/board_moderator.ex b/lib/epochtalk_server/models/board_moderator.ex index 5a29deb8..6c450fd3 100644 --- a/lib/epochtalk_server/models/board_moderator.ex +++ b/lib/epochtalk_server/models/board_moderator.ex @@ -6,7 +6,10 @@ defmodule EpochtalkServer.Models.BoardModerator do @moduledoc """ `BoardModerator` model, for performing actions relating to `Board` moderators """ - + @type t :: %__MODULE__{ + user_id: non_neg_integer, + board_id: non_neg_integer + } @primary_key false schema "board_moderators" do belongs_to :user, User @@ -19,9 +22,9 @@ defmodule EpochtalkServer.Models.BoardModerator do Create generic changeset for `BoardModerator` model """ @spec changeset( - board_moderator :: %EpochtalkServer.Models.BoardModerator{}, + board_moderator :: t(), attrs :: %{} | nil - ) :: %EpochtalkServer.Models.Board{} + ) :: Board.t() def changeset(board_moderator, attrs \\ %{}) do board_moderator |> cast(attrs, [:user_id, :board_id]) diff --git a/lib/epochtalk_server/models/category.ex b/lib/epochtalk_server/models/category.ex index df82abd7..25334627 100644 --- a/lib/epochtalk_server/models/category.ex +++ b/lib/epochtalk_server/models/category.ex @@ -8,7 +8,17 @@ defmodule EpochtalkServer.Models.Category do @moduledoc """ `Category` model, for performing actions relating to forum categories """ - + @type t :: %__MODULE__{ + name: String.t(), + view_order: non_neg_integer, + viewable_by: non_neg_integer, + postable_by: non_neg_integer, + created_at: NaiveDateTime.t(), + imported_at: NaiveDateTime.t(), + updated_at: NaiveDateTime.t(), + meta: %{}, + boards: [Board.t()] + } schema "categories" do field :name, :string field :view_order, :integer @@ -27,9 +37,9 @@ defmodule EpochtalkServer.Models.Category do Create generic changeset for `Category` model """ @spec changeset( - category :: %EpochtalkServer.Models.Category{}, + category :: t(), attrs :: %{} | nil - ) :: %EpochtalkServer.Models.Category{} + ) :: t() def changeset(category, attrs) do category |> cast(attrs, [:id, :name, :view_order, :viewable_by, :postable_by, :created_at, :imported_at, :updated_at, :meta]) @@ -40,9 +50,9 @@ defmodule EpochtalkServer.Models.Category do Creates changeset for inserting a new `Category` model """ @spec create_changeset( - category :: %EpochtalkServer.Models.Category{}, + category :: t(), attrs :: %{} | nil - ) :: %EpochtalkServer.Models.Category{} + ) :: t() def create_changeset(category, attrs) do now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) attrs = attrs @@ -56,9 +66,9 @@ defmodule EpochtalkServer.Models.Category do Creates changeset for updating an existing `Category` model """ @spec create_changeset( - category :: %EpochtalkServer.Models.Category{}, + category :: t(), attrs :: %{} | nil - ) :: %EpochtalkServer.Models.Category{} + ) :: t() def update_for_board_mapping_changeset(category, attrs) do category |> cast(attrs, [:id, :name, :view_order, :viewable_by]) @@ -72,7 +82,7 @@ defmodule EpochtalkServer.Models.Category do """ @spec create( category_attrs :: %{} - ) :: {:ok, category :: %EpochtalkServer.Models.Category{}} | {:error, Ecto.Changeset.t()} + ) :: {:ok, category :: t()} | {:error, Ecto.Changeset.t()} def create(attrs) do category_cs = create_changeset(%Category{}, attrs) Repo.insert(category_cs) @@ -83,7 +93,7 @@ defmodule EpochtalkServer.Models.Category do """ @spec update_for_board_mapping( category_map :: %{ id: id :: integer } - ) :: {:ok, category :: %EpochtalkServer.Models.Category{}} | {:error, Ecto.Changeset.t()} + ) :: {:ok, category :: t()} | {:error, Ecto.Changeset.t()} def update_for_board_mapping(%{ id: id } = category_map) do %Category{ id: id } |> update_for_board_mapping_changeset(category_map) diff --git a/lib/epochtalk_server/models/invitation.ex b/lib/epochtalk_server/models/invitation.ex index b050bcf2..706a38bf 100644 --- a/lib/epochtalk_server/models/invitation.ex +++ b/lib/epochtalk_server/models/invitation.ex @@ -8,6 +8,11 @@ defmodule EpochtalkServer.Models.Invitation do `Invitation` model, for performing actions relating to inviting new users to the forum """ + @type t :: %__MODULE__{ + email: String.t(), + hash: String.t(), + created_at: NaiveDateTime.t() + } @primary_key false schema "invitations" do field :email, :string @@ -21,9 +26,9 @@ defmodule EpochtalkServer.Models.Invitation do Create changeset for inserting a new `Invitation` model """ @spec create_changeset( - invitation :: %EpochtalkServer.Models.Invitation{}, + invitation :: t(), attrs :: %{} | nil - ) :: %EpochtalkServer.Models.Invitation{} + ) :: t() def create_changeset(invitation, attrs \\ %{}) do now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) attrs = attrs @@ -46,7 +51,7 @@ defmodule EpochtalkServer.Models.Invitation do """ @spec create( email :: String.t() - ) :: {:ok, invitation :: %EpochtalkServer.Models.Invitation{}} | {:error, Ecto.Changeset.t()} + ) :: {:ok, invitation :: t()} | {:error, Ecto.Changeset.t()} def create(email), do: Repo.insert(create_changeset(%Invitation{}, %{email: email})) @doc """ diff --git a/lib/epochtalk_server/models/metadata_board.ex b/lib/epochtalk_server/models/metadata_board.ex index ecb69f87..40da3f3a 100644 --- a/lib/epochtalk_server/models/metadata_board.ex +++ b/lib/epochtalk_server/models/metadata_board.ex @@ -8,6 +8,18 @@ defmodule EpochtalkServer.Models.MetadataBoard do `MetadataBoard` model, for performing actions relating to `Board` metadata """ + @type t :: %__MODULE__{ + board: Board.t(), + post_count: non_neg_integer, + thread_count: non_neg_integer, + total_post: non_neg_integer, + total_thread_count: non_neg_integer, + last_post_username: String.t(), + last_post_created_at: NaiveDateTime.t(), + last_thread_id: non_neg_integer, + last_thread_title: String.t(), + last_post_position: non_neg_integer + } @schema_prefix "metadata" schema "boards" do belongs_to :board, Board @@ -28,9 +40,9 @@ defmodule EpochtalkServer.Models.MetadataBoard do Create changeset for inserting a new `MetadataBoard` model """ @spec changeset( - metadata_board :: %EpochtalkServer.Models.MetadataBoard{}, + metadata_board :: t(), attrs :: %{} | nil - ) :: %EpochtalkServer.Models.MetadataBoard{} + ) :: t() def changeset(metadata_board, attrs \\ %{}) do metadata_board |> cast(attrs, [:id, :board_id, :post_count, :thread_count, @@ -46,7 +58,7 @@ defmodule EpochtalkServer.Models.MetadataBoard do Inserts a new `MetadataBoard` into the database """ @spec insert( - metadata_board :: %EpochtalkServer.Models.MetadataBoard{} - ) :: {:ok, metadata_board :: %EpochtalkServer.Models.MetadataBoard{}} | {:error, Ecto.Changeset.t()} + metadata_board :: t() + ) :: {:ok, metadata_board :: t()} | {:error, Ecto.Changeset.t()} def insert(%MetadataBoard{} = metadata_board), do: Repo.insert(metadata_board) end diff --git a/lib/epochtalk_server/models/permission.ex b/lib/epochtalk_server/models/permission.ex index 1f87320e..8ef51cd0 100644 --- a/lib/epochtalk_server/models/permission.ex +++ b/lib/epochtalk_server/models/permission.ex @@ -7,6 +7,9 @@ defmodule EpochtalkServer.Models.Permission do `Permission` model, for performing actions relating to `Role` permissions, used for seeding """ + @type t :: %__MODULE__{ + path: String.t() + } @primary_key false schema "permissions" do field :path, :string @@ -18,9 +21,9 @@ defmodule EpochtalkServer.Models.Permission do Creates a generic changeset for `Permission` model """ @spec changeset( - permission :: %EpochtalkServer.Models.Permission{}, + permission :: t(), attrs :: %{} | nil - ) :: %EpochtalkServer.Models.Permission{} + ) :: t() def changeset(permission, attrs \\ %{}) do permission |> cast(attrs, [:path]) @@ -32,12 +35,12 @@ defmodule EpochtalkServer.Models.Permission do @doc """ Returns every `Permission` record in the database """ - @spec all() :: [%EpochtalkServer.Models.Permission{}] | [] + @spec all() :: [t()] | [] def all(), do: Repo.all(Permission) @doc """ Returns a specific `Permission` provided it's path """ - @spec by_path(path :: String.t()) :: %EpochtalkServer.Models.Permission{} | nil + @spec by_path(path :: String.t()) :: t() | nil def by_path(path) when is_binary(path), do: Repo.get_by(Permission, path: path) end diff --git a/lib/epochtalk_server/models/preference.ex b/lib/epochtalk_server/models/preference.ex index 2ba7ab5e..041889b4 100644 --- a/lib/epochtalk_server/models/preference.ex +++ b/lib/epochtalk_server/models/preference.ex @@ -5,7 +5,19 @@ defmodule EpochtalkServer.Models.Preference do @moduledoc """ `Preference` model, for performing actions relating to a user's preferences """ - + @type t :: %__MODULE__{ + user_id: non_neg_integer, + posts_per_page: non_neg_integer, + threads_per_page: non_neg_integer, + collapsed_categories: %{}, + ignored_boards: %{}, + timezone_offset: String.t(), + notify_replied_threads: boolean, + ignore_newbies: boolean, + patroller_view: boolean, + email_mentions: boolean, + email_messages: boolean + } @primary_key false @schema_prefix "users" schema "preferences" do @@ -28,9 +40,9 @@ defmodule EpochtalkServer.Models.Preference do Creates a generic changeset for `Preference` model """ @spec changeset( - preference :: %EpochtalkServer.Models.Preference{}, + preference :: t(), attrs :: %{} | nil - ) :: %EpochtalkServer.Models.Preference{} + ) :: t() def changeset(preference, attrs \\ %{}) do preference |> cast(attrs, [:user_id, :posts_per_page, :threads_per_page, diff --git a/lib/epochtalk_server/models/profile.ex b/lib/epochtalk_server/models/profile.ex index c1840188..1aba077a 100644 --- a/lib/epochtalk_server/models/profile.ex +++ b/lib/epochtalk_server/models/profile.ex @@ -5,7 +5,16 @@ defmodule EpochtalkServer.Models.Profile do @moduledoc """ `Profile` model, for performing actions relating a user's profile """ - + @type t :: %__MODULE__{ + user_id: non_neg_integer, + avatar: String.t(), + position: String.t(), + signature: String.t(), + raw_signature: String.t(), + post_count: non_neg_integer, + fields: %{}, + last_active: NaiveDateTime.t() + } @schema_prefix "users" schema "profiles" do belongs_to :user, User @@ -24,9 +33,9 @@ defmodule EpochtalkServer.Models.Profile do Creates a generic changeset for `Profile` model """ @spec changeset( - profile :: %EpochtalkServer.Models.Profile{}, + profile :: t(), attrs :: %{} | nil - ) :: %EpochtalkServer.Models.Profile{} + ) :: t() def changeset(profile, attrs \\ %{}) do profile |> cast(attrs, [:user_id, :avatar, :position, :signature, :raw_signature, :post_count, :field, :last_active]) diff --git a/lib/epochtalk_server/models/role.ex b/lib/epochtalk_server/models/role.ex index 1d4a69e5..32740873 100644 --- a/lib/epochtalk_server/models/role.ex +++ b/lib/epochtalk_server/models/role.ex @@ -9,7 +9,17 @@ defmodule EpochtalkServer.Models.Role do @moduledoc """ `Role` model, for performing actions relating to user roles """ - + @type t :: %__MODULE__{ + name: String.t(), + description: String.t(), + lookup: String.t(), + priority: non_neg_integer, + highlight_color: String.t(), + permissions: %{}, + priority_restrictions: [non_neg_integer], + created_at: NaiveDateTime.t(), + updated_at: NaiveDateTime.t() + } @derive {Jason.Encoder, only: [:name, :description, :lookup, :priority, :highlight_color, :permissions, :priority_restrictions, :created_at, :updated_at]} schema "roles" do field :name, :string @@ -31,9 +41,9 @@ defmodule EpochtalkServer.Models.Role do Create generic changeset for the `Role` model """ @spec changeset( - role :: %EpochtalkServer.Models.Role{}, + role :: t(), attrs :: %{} | nil - ) :: %EpochtalkServer.Models.Role{} + ) :: t() def changeset(role, attrs \\ %{}) do role |> cast(attrs, [:name, :description, :lookup, :priority, :permissions]) @@ -47,7 +57,7 @@ defmodule EpochtalkServer.Models.Role do @doc """ Returns every `Role` record in the database """ - @spec all() :: [%EpochtalkServer.Models.Role{}] | [] + @spec all() :: [t()] | [] def all, do: from(r in Role, order_by: r.id) |> Repo.all @doc """ @@ -65,7 +75,7 @@ defmodule EpochtalkServer.Models.Role do @doc """ Returns default `Role`, for base installation this is the `user` role """ - @spec get_default() :: %EpochtalkServer.Models.Role{} | nil + @spec get_default() :: t() | nil def get_default(), do: by_lookup("user") @doc """ @@ -73,7 +83,7 @@ defmodule EpochtalkServer.Models.Role do """ @spec by_lookup( lookup_or_lookups :: String.t() | [String.t()] - ) :: %EpochtalkServer.Models.Role{} | [%EpochtalkServer.Models.Role{}] | [] | nil + ) :: t() | [t()] | [] | nil def by_lookup(lookups) when is_list(lookups) do from(r in Role, where: r.lookup in ^lookups) |> Repo.all end @@ -84,7 +94,7 @@ defmodule EpochtalkServer.Models.Role do """ @spec by_user_id( user_id :: integer - ) :: [%EpochtalkServer.Models.Role{}] + ) :: [t()] def by_user_id(user_id) do query = from ru in RoleUser, join: r in Role, @@ -106,8 +116,8 @@ defmodule EpochtalkServer.Models.Role do Inserts a new `Role` into the database """ @spec insert( - role_or_roles :: %EpochtalkServer.Models.Role{} | [%{}] - ) :: {:ok, role :: %EpochtalkServer.Models.Role{}} | {non_neg_integer(), nil | [term()]} | {:error, Ecto.Changeset.t()} + role_or_roles :: t() | [%{}] + ) :: {:ok, role :: t()} | {non_neg_integer(), nil | [term()]} | {:error, Ecto.Changeset.t()} def insert([]), do: {:error, "Role list is empty"} def insert(%Role{} = role), do: Repo.insert(role) def insert([%{}|_] = roles), do: Repo.insert_all(Role, roles) @@ -120,7 +130,7 @@ defmodule EpochtalkServer.Models.Role do @spec set_permissions( id :: integer, permissions_attrs :: %{} - ) :: {:ok, role :: %EpochtalkServer.Models.Role{}} | {:error, Ecto.Changeset.t()} + ) :: {:ok, role :: t()} | {:error, Ecto.Changeset.t()} def set_permissions(id, permissions) do Role |> Repo.get(id) @@ -140,8 +150,8 @@ defmodule EpochtalkServer.Models.Role do and is expected to return the updated user's roles. """ @spec handle_empty_user_roles( - user :: %EpochtalkServer.Models.User{} - ) :: %EpochtalkServer.Models.User{} + user :: User.t() + ) :: User.t() def handle_empty_user_roles(%User{roles: [%Role{} | _]} = user), do: user def handle_empty_user_roles(%User{roles: []} = user), do: user |> Map.put(:roles, [Role.get_default()]) def handle_empty_user_roles(%User{} = user), do: user |> Map.put(:roles, [Role.get_default()]) @@ -154,8 +164,8 @@ defmodule EpochtalkServer.Models.Role do and is expected to return the updated user's roles. """ @spec handle_banned_user_role( - user_or_roles :: %EpochtalkServer.Models.User{} | [%EpochtalkServer.Models.Role{}] - ) :: %EpochtalkServer.Models.User{} | [%EpochtalkServer.Models.Role{}] + user_or_roles :: User.t() | [t()] + ) :: User.t() | [t()] # called with user model, outputs user model with updated role def handle_banned_user_role(%User{roles: [%Role{} | _] = roles} = user) do if banned_role = Enum.find(roles, &(&1.lookup == "banned")), @@ -170,7 +180,7 @@ defmodule EpochtalkServer.Models.Role do Takes in list of user's roles, and returns an xored map of all `Role` permissions """ @spec get_masked_permissions( - roles :: [%EpochtalkServer.Models.Role{}] + roles :: [t()] ) :: %{} # Given %User{}.roles, outputs xored permissions def get_masked_permissions(roles) when is_list(roles), do: Enum.reduce(roles, %{}, &mask_permissions(&2, &1)) diff --git a/lib/epochtalk_server/models/role_permission.ex b/lib/epochtalk_server/models/role_permission.ex index 5aed7c61..8f82d739 100644 --- a/lib/epochtalk_server/models/role_permission.ex +++ b/lib/epochtalk_server/models/role_permission.ex @@ -10,6 +10,12 @@ defmodule EpochtalkServer.Models.RolePermission do `RolePermission` model, for performing actions relating to a roles permissions """ + @type t :: %__MODULE__{ + role: Role.t(), + permission: Permission.t(), + value: boolean, + modified: boolean + } @primary_key false schema "roles_permissions" do belongs_to :role, Role, foreign_key: :role_id, type: :integer @@ -27,9 +33,9 @@ defmodule EpochtalkServer.Models.RolePermission do Creates a generic changeset for `RolePermission` model """ @spec changeset( - role_permission :: %EpochtalkServer.Models.RolePermission{}, + role_permission :: t(), attrs :: %{} | nil - ) :: %EpochtalkServer.Models.RolePermission{} + ) :: t() def changeset(role_permission, attrs \\ %{}) do role_permission |> cast(attrs, [:role_id, :permission_path, :value, :modified]) @@ -42,8 +48,8 @@ defmodule EpochtalkServer.Models.RolePermission do Inserts a new `RolePermission` into the database """ @spec insert( - role_permission_or_role_permissions :: %EpochtalkServer.Models.RolePermission{} | [%{}] - ) :: {:ok, role :: %EpochtalkServer.Models.RolePermission{}} | {non_neg_integer(), nil | [term()]} | {:error, Ecto.Changeset.t()} + role_permission_or_role_permissions :: t() | [%{}] + ) :: {:ok, role :: t()} | {non_neg_integer(), nil | [term()]} | {:error, Ecto.Changeset.t()} def insert([]), do: {:error, "Role permission list is empty"} def insert(%RolePermission{} = role_permission), do: Repo.insert(role_permission) def insert([%{}|_] = roles_permissions), do: Repo.insert_all(RolePermission, roles_permissions) diff --git a/lib/epochtalk_server/models/role_user.ex b/lib/epochtalk_server/models/role_user.ex index f93d706a..2fbbe9b6 100644 --- a/lib/epochtalk_server/models/role_user.ex +++ b/lib/epochtalk_server/models/role_user.ex @@ -11,6 +11,11 @@ defmodule EpochtalkServer.Models.RoleUser do """ @admin_role_id 1 + + @type t :: %__MODULE__{ + user: User.t(), + role: Role.t() + } @primary_key false schema "roles_users" do belongs_to :user, User @@ -23,9 +28,9 @@ defmodule EpochtalkServer.Models.RoleUser do Creates a generic changeset for `RoleUser` model """ @spec changeset( - role_user :: %EpochtalkServer.Models.RoleUser{}, + role_user :: t(), attrs :: %{} | nil - ) :: %EpochtalkServer.Models.RoleUser{} + ) :: t() def changeset(role_user, attrs \\ %{}) do role_user |> cast(attrs, [:user_id, :role_id]) @@ -39,7 +44,7 @@ defmodule EpochtalkServer.Models.RoleUser do """ @spec set_admin( user_id :: integer - ) :: {:ok, role_user :: %EpochtalkServer.Models.RoleUser{}} | {:error, Ecto.Changeset.t()} + ) :: {:ok, role_user :: t()} | {:error, Ecto.Changeset.t()} def set_admin(user_id), do: set_user_role(@admin_role_id, user_id) @doc """ @@ -48,7 +53,7 @@ defmodule EpochtalkServer.Models.RoleUser do @spec set_user_role( role_id :: integer, user_id :: integer - ) :: {:ok, role_user :: %EpochtalkServer.Models.RoleUser{}} | {:error, Ecto.Changeset.t()} + ) :: {:ok, role_user :: t()} | {:error, Ecto.Changeset.t()} def set_user_role(role_id, user_id) do case Repo.one(from(ru in RoleUser, where: ru.role_id == ^role_id and ru.user_id == ^user_id)) do nil -> Repo.insert(changeset(%RoleUser{}, %{role_id: role_id, user_id: user_id})) diff --git a/lib/epochtalk_server/models/user.ex b/lib/epochtalk_server/models/user.ex index 566a8fa3..6b00af30 100644 --- a/lib/epochtalk_server/models/user.ex +++ b/lib/epochtalk_server/models/user.ex @@ -14,7 +14,27 @@ defmodule EpochtalkServer.Models.User do @moduledoc """ `User` model, for performing actions relating a user """ - + @type t :: %__MODULE__{ + id: non_neg_integer, + email: String.t(), + username: String.t(), + password: String.t(), + password_confirmation: String.t(), + passhash: String.t(), + confirmation_token: String.t(), + reset_token: String.t(), + reset_expiration: String.t(), + deleted: boolean, + malicious_score: float, + created_at: NaiveDateTime.t(), + imported_at: NaiveDateTime.t(), + updated_at: NaiveDateTime.t(), + preferences: Preference.t(), + profile: Profile.t(), + ban_info: Ban.t(), + roles: [Role.t()], + moderating: [BoardModerator.t()] + } schema "users" do field :email, :string field :username, :string @@ -46,9 +66,9 @@ defmodule EpochtalkServer.Models.User do if validation of username, email and password do not pass. """ @spec registration_changeset( - user :: %EpochtalkServer.Models.User{}, + user :: t(), attrs :: %{} | nil - ) :: %EpochtalkServer.Models.User{} + ) :: t() def registration_changeset(user, attrs) do user |> cast(attrs, [:id, :email, :username, :created_at, :updated_at, :deleted, :malicious_score, :password]) @@ -65,7 +85,7 @@ defmodule EpochtalkServer.Models.User do """ @spec create( attrs :: %{} - ) :: {:ok, user :: %EpochtalkServer.Models.User{}} | {:error, Ecto.Changeset.t()} + ) :: {:ok, user :: t()} | {:error, Ecto.Changeset.t()} def create(attrs) do user_cs = User.registration_changeset(%User{}, attrs) case Repo.insert(user_cs) do @@ -84,7 +104,7 @@ defmodule EpochtalkServer.Models.User do @spec create( attrs :: %{}, admin :: boolean - ) :: {:ok, user :: %EpochtalkServer.Models.User{}} | {:error, Ecto.Changeset.t()} + ) :: {:ok, user :: t()} | {:error, Ecto.Changeset.t()} def create(attrs, true = _admin) do Repo.transaction(fn -> {:ok, user} = create(attrs) @@ -113,7 +133,7 @@ defmodule EpochtalkServer.Models.User do """ @spec by_id( id :: integer - ) :: %EpochtalkServer.Models.User{} | nil + ) :: t() | nil def by_id(id) when is_integer(id), do: Repo.get_by(User, id: id) @doc """ @@ -131,7 +151,7 @@ defmodule EpochtalkServer.Models.User do """ @spec by_username( username :: String.t() - ) :: {:ok, user :: %EpochtalkServer.Models.User{}} | {:error, :user_not_found} + ) :: {:ok, user :: t()} | {:error, :user_not_found} def by_username(username) when is_binary(username) do query = from u in User, where: u.username == ^username, @@ -148,9 +168,9 @@ defmodule EpochtalkServer.Models.User do and in place. Otherwise the user is just returned with no change. """ @spec handle_malicious_user( - user :: %EpochtalkServer.Models.User{}, + user :: t(), ip :: tuple - ) :: {:ok, user :: %EpochtalkServer.Models.User{}} | {:error, :ban_error} + ) :: {:ok, user :: t()} | {:error, :ban_error} def handle_malicious_user(%User{} = user, ip) do # convert ip tuple into string ip_str = ip |> :inet_parse.ntoa |> to_string @@ -170,7 +190,7 @@ defmodule EpochtalkServer.Models.User do Validates with Argon2 that a `User` `passhash` matches the supplied `password` """ @spec valid_password?( - user :: %EpochtalkServer.Models.User{}, + user :: t(), password :: String.t() ) :: true | false def valid_password?(%User{passhash: hashed_password} = _user, password) From 63aef6954302c0d48f21659cd2b5ff115bebf2ea Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 13 Oct 2022 14:35:10 -1000 Subject: [PATCH 210/231] feat(redix-configs): add configurations for redix instead of hardcoding --- config/config.exs | 9 +++++++-- lib/epochtalk_server/application.ex | 5 ++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/config/config.exs b/config/config.exs index f79928e1..61fe247b 100644 --- a/config/config.exs +++ b/config/config.exs @@ -22,13 +22,18 @@ config :guardian, Guardian.DB, # schema_name: "guardian_tokens" # default # token_types: ["refresh_token"] # store all token types if not set -# Configure GuardianRedis +# Configure GuardianRedis (for auth) # (implementation of Guardian.DB storage in redis) config :guardian_redis, :redis, - host: "127.0.0.1", + host: System.get_env("REDIS_HOST") || "127.0.0.1", port: 6379, pool_size: 10 +# Configures redix (for sessions storage) +config :epochtalk_server, :redix, + host: System.get_env("REDIS_HOST") || "127.0.0.1", + name: :redix + # Configures the endpoint config :epochtalk_server, EpochtalkServerWeb.Endpoint, url: [host: "localhost"], diff --git a/lib/epochtalk_server/application.ex b/lib/epochtalk_server/application.ex index ea57355b..ee90dee8 100644 --- a/lib/epochtalk_server/application.ex +++ b/lib/epochtalk_server/application.ex @@ -11,7 +11,7 @@ defmodule EpochtalkServer.Application do # Start Guardian Redis Redix connection GuardianRedis.Redix, # Start the server Redis connection - {Redix, host: "localhost", name: :redix}, + {Redix, host: redix_config()[:host], name: redix_config()[:name]}, # Start the Ecto repository EpochtalkServer.Repo, # Start the Telemetry supervisor @@ -37,4 +37,7 @@ defmodule EpochtalkServer.Application do EpochtalkServerWeb.Endpoint.config_change(changed, removed) :ok end + + # fetch redix config + defp redix_config, do: Application.get_env(:epochtalk_server, :redix) end From e8b3dc75cfa1fa28a94ec0e2697d72fd6af03b8a Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 13 Oct 2022 15:33:05 -1000 Subject: [PATCH 211/231] feat(smtp-mailer): hook up smtp mailer for development --- .gitignore | 3 +++ config/dev.exs | 20 ++++++++++++++++++++ mix.exs | 9 +++++---- mix.lock | 3 ++- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 24f24856..a5795582 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ epochtalk_server-*.tar # Ignore generated seed files priv/repo/seeds/permissions.json + +# Ignore secret config files +config/*.secret.exs diff --git a/config/dev.exs b/config/dev.exs index 53dcf840..bc2d1fb9 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -26,6 +26,22 @@ config :epochtalk_server, EpochtalkServerWeb.Endpoint, secret_key_base: "9ORa6oGSN+xlXNedSn0gIKVc/6//naQqSiZsRJ8vNbcvHpPOTPMLgcn134WIH3Pd", watchers: [] +config :epochtalk_server, EpochtalkServer.Mailer, + adapter: Swoosh.Adapters.SMTP, + relay: "smtp.example.com", + username: "username", + password: "password", + ssl: true, + tls: :if_available, + auth: :always, + port: 465, + retries: 2, + no_mx_lookups: false + # dkim: [ + # s: "default", d: "domain.com", + # private_key: {:pem_plain, File.read!("priv/keys/domain.private")} + # ] + # ## SSL Support # # In order to use HTTPS in development, a self-signed @@ -59,3 +75,7 @@ config :phoenix, :stacktrace_depth, 20 # Initialize plugs at runtime for faster development compilation config :phoenix, :plug_init_mode, :runtime + +if File.exists?("config/dev.secret.exs") do + import_config "dev.secret.exs" +end diff --git a/mix.exs b/mix.exs index 850b9340..5cec6b8c 100644 --- a/mix.exs +++ b/mix.exs @@ -37,9 +37,10 @@ defmodule EpochtalkServer.MixProject do {:corsica, "~> 1.2.0"}, {:ecto_sql, "~> 3.6"}, {:ex_doc, "~> 0.28.5"}, - {:guardian, "~> 2.2.4"}, - {:guardian_db, "~> 2.1.0"}, - {:guardian_redis, "~> 0.1.0"}, + {:gen_smtp, "~> 1.2"}, + {:guardian, "~> 2.2"}, + {:guardian_db, "~> 2.1"}, + {:guardian_redis, "~> 0.1"}, {:iteraptor, git: "https://github.com/epochtalk/elixir-iteraptor.git", tag: "1.13.1"}, {:jason, "~> 1.3.0"}, {:phoenix, "~> 1.6.11"}, @@ -48,7 +49,7 @@ defmodule EpochtalkServer.MixProject do {:postgrex, ">= 0.0.0"}, {:redix, "~> 1.1.5"}, {:remote_ip, "~> 1.0.0"}, - {:swoosh, "~> 1.3"}, + {:swoosh, "~> 1.8"}, {:telemetry_metrics, "~> 0.6"}, {:telemetry_poller, "~> 1.0"}, {:uuid, "~> 1.1.8"} diff --git a/mix.lock b/mix.lock index 9ce9cd58..6b871f1d 100644 --- a/mix.lock +++ b/mix.lock @@ -14,6 +14,7 @@ "ecto_sql": {:hex, :ecto_sql, "3.8.3", "a7d22c624202546a39d615ed7a6b784580391e65723f2d24f65941b4dd73d471", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.8.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "348cb17fb9e6daf6f251a87049eafcb57805e2892e5e6a0f5dea0985d367329b"}, "elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"}, "ex_doc": {:hex, :ex_doc, "0.28.5", "3e52a6d2130ce74d096859e477b97080c156d0926701c13870a4e1f752363279", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "d2c4b07133113e9aa3e9ba27efb9088ba900e9e51caa383919676afdf09ab181"}, + "gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"}, "guardian": {:hex, :guardian, "2.2.4", "3dafdc19665411c96b2796d184064d691bc08813a132da5119e39302a252b755", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "6f83d4309c16ec2469da8606bb2a9815512cc2fac1595ad34b79940a224eb110"}, "guardian_db": {:hex, :guardian_db, "2.1.0", "ec95a9d99cdd1e550555d09a7bb4a340d8887aad0697f594590c2fd74be02426", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:guardian, "~> 1.0 or ~> 2.0", [hex: :guardian, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "f8e7d543ac92c395f3a7fd5acbe6829faeade57d688f7562e2f0fca8f94a0d70"}, "guardian_redis": {:hex, :guardian_redis, "0.1.0", "705f8291346d813755a701e0241acaa845453d662aca675f1de8f0821554a6f9", [:mix], [{:guardian_db, "~> 2.0", [hex: :guardian_db, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:redix, "~> 1.0", [hex: :redix, repo: "hexpm", optional: false]}], "hexpm", "8058f6613f888a2f10836f6b669be3f21c7d303955862b638fc3c1bafaf66bef"}, @@ -36,7 +37,7 @@ "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "redix": {:hex, :redix, "1.1.5", "6fc460d66a5c2287e83e6d73dddc8d527ff59cb4d4f298b41e03a4db8c3b2bd5", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "679afdd4c14502fe9c11387ff1cdcb33065a1cf511097da1eee407f17c7a418b"}, "remote_ip": {:hex, :remote_ip, "1.0.0", "3d7fb45204a5704443f480cee9515e464997f52c35e0a60b6ece1f81484067ae", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9e9fcad4e50c43b5234bb6a9629ed6ab223f3ed07147bd35470e4ee5c8caf907"}, - "swoosh": {:hex, :swoosh, "1.7.4", "f967d9b2659e81bab241b96267aae1001d35c2beea2df9c03dcf47b007bf566f", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1553d994b4cf069162965e63de1e1c53d8236e127118d21e56ce2abeaa3f25b4"}, + "swoosh": {:hex, :swoosh, "1.8.1", "b1694d57c01852f50f7d4e6a74f5d119b2d8722d6a82f7288703c3e448ddbbf8", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e64a93d71d1e1e32db681cf7870697495c9cb2df4a5484f4d91ded326ccd3cbb"}, "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, From 321fee4cae137438ca939589bbc1cb47cdc6adcf Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 13 Oct 2022 15:34:34 -1000 Subject: [PATCH 212/231] feat(configurations): reorganize configurations, use local mailer for dev mode --- config/config.exs | 33 ++++++++++++++++++++++-------- config/dev.exs | 17 ++------------- config/runtime.exs | 23 +++++++++++++++++++++ lib/epochtalk_server/mailer.ex | 6 ++++++ lib/epochtalk_server_web/router.ex | 7 +++++-- 5 files changed, 61 insertions(+), 25 deletions(-) diff --git a/config/config.exs b/config/config.exs index 61fe247b..9bef11e6 100644 --- a/config/config.exs +++ b/config/config.exs @@ -13,7 +13,6 @@ config :epochtalk_server, # Configure Guardian config :epochtalk_server, EpochtalkServer.Auth.Guardian, issuer: "EpochtalkServer", - # TODO: configure this at runtime through env secret_key: "Secret key. You can use `mix guardian.gen.secret` to get one" # Configure Guardian.DB @@ -25,13 +24,13 @@ config :guardian, Guardian.DB, # Configure GuardianRedis (for auth) # (implementation of Guardian.DB storage in redis) config :guardian_redis, :redis, - host: System.get_env("REDIS_HOST") || "127.0.0.1", + host: "127.0.0.1", port: 6379, pool_size: 10 # Configures redix (for sessions storage) config :epochtalk_server, :redix, - host: System.get_env("REDIS_HOST") || "127.0.0.1", + host: "127.0.0.1", name: :redix # Configures the endpoint @@ -48,12 +47,30 @@ config :epochtalk_server, EpochtalkServerWeb.Endpoint, # Configures the mailer # -# By default it uses the "Local" adapter which stores the emails -# locally. You can see the emails in your browser, at "/dev/mailbox". +# By default "SMTP" adapter is being used. # -# For production it's recommended to configure a different adapter -# at the `config/runtime.exs`. -config :epochtalk_server, EpochtalkServer.Mailer, adapter: Swoosh.Adapters.Local +# For Development `config/dev.exs` loads the "Local" adapter which allows preview +# of sent emails at the url `/dev/mailbox`. To test SMTP in Development mode, +# mailer configurations for adapter, credentials and other options can be +# overriden in `config/dev.secret.exs` +# +# For production configurations are fetched from system environment variables. +# Overrides for production are in `config/runtime.exs`. +config :epochtalk_server, EpochtalkServer.Mailer, + adapter: Swoosh.Adapters.SMTP, + relay: "smtp.example.com", + username: "username", + password: "password", + ssl: true, + tls: :if_available, + auth: :always, + port: 465, + retries: 2, + no_mx_lookups: false + # dkim: [ + # s: "default", d: "domain.com", + # private_key: {:pem_plain, File.read!("priv/keys/domain.private")} + # ] # Swoosh API client is needed for adapters other than SMTP. config :swoosh, :api_client, false diff --git a/config/dev.exs b/config/dev.exs index bc2d1fb9..65d1492f 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -26,21 +26,8 @@ config :epochtalk_server, EpochtalkServerWeb.Endpoint, secret_key_base: "9ORa6oGSN+xlXNedSn0gIKVc/6//naQqSiZsRJ8vNbcvHpPOTPMLgcn134WIH3Pd", watchers: [] -config :epochtalk_server, EpochtalkServer.Mailer, - adapter: Swoosh.Adapters.SMTP, - relay: "smtp.example.com", - username: "username", - password: "password", - ssl: true, - tls: :if_available, - auth: :always, - port: 465, - retries: 2, - no_mx_lookups: false - # dkim: [ - # s: "default", d: "domain.com", - # private_key: {:pem_plain, File.read!("priv/keys/domain.private")} - # ] +# Configure Local Mailer by default for dev mode (this can be overridden in dev.secret.exs) +config :epochtalk_server, EpochtalkServer.Mailer, adapter: Swoosh.Adapters.Local # ## SSL Support # diff --git a/config/runtime.exs b/config/runtime.exs index 6af9ebda..670bbe2f 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -80,4 +80,27 @@ if config_env() == :prod do # config :swoosh, :api_client, Swoosh.ApiClient.Hackney # # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details. + config :epochtalk_server, EpochtalkServer.Mailer, + relay: System.get_env("EMAILER_SMTP_RELAY") || "smtp.example.com", + username: System.get_env("EMAILER_SMTP_USERNAME") || "username", + password: System.get_env("EMAILER_SMTP_PASSWORD") || "password", + port: System.get_env("EMAILER_SMTP_PORT") || 465 + + # Configure Guardian for Runtime + config :epochtalk_server, EpochtalkServer.Auth.Guardian, + secret_key: System.get_env("GUARDIAN_SECRET_KEY") || + raise """ + environment variable GUARDIAN_SECRET_KEY is missing. + You can generate one by calling: mix guardian.gen.secret + """ + + # Configure Guardian Redis + config :guardian_redis, :redis, + host: System.get_env("REDIS_HOST") || "127.0.0.1", + port: String.to_integer(System.get_env("REDIS_PORT") || "6379"), + pool_size: String.to_integer(System.get_env("REDIS_POOL_SIZE") || "10") + + # Configure Redis for Session Storage + config :epochtalk_server, :redix, + host: System.get_env("REDIS_HOST") || "127.0.0.1" end diff --git a/lib/epochtalk_server/mailer.ex b/lib/epochtalk_server/mailer.ex index ddc899c6..aecd61fc 100644 --- a/lib/epochtalk_server/mailer.ex +++ b/lib/epochtalk_server/mailer.ex @@ -1,3 +1,9 @@ defmodule EpochtalkServer.Mailer do use Swoosh.Mailer, otp_app: :epochtalk_server + import Swoosh.Email + + def test_email() do + email = new(from: {"Dr B Banner", "hulk.smash@example.com"}, to: {"Tony Stark", "iron.man@mailinator.com"}, subject: "Hello, Avengers!", text_body: "HELLO WORLD") + deliver(email) + end end diff --git a/lib/epochtalk_server_web/router.ex b/lib/epochtalk_server_web/router.ex index 6ea84c12..21de2e50 100644 --- a/lib/epochtalk_server_web/router.ex +++ b/lib/epochtalk_server_web/router.ex @@ -34,9 +34,12 @@ defmodule EpochtalkServerWeb.Router do # # Note that preview only shows emails that were sent by the same # node running the Phoenix server. - if Mix.env() == :dev do + if Mix.env == :dev do + pipeline :browser do + plug :accepts, ["html"] + end scope "/dev" do - pipe_through [:fetch_session, :protect_from_forgery] + pipe_through [:browser] forward "/mailbox", Plug.Swoosh.MailboxPreview end end From d48e662632e63d68c8b25b81dd56917d046dba50 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 13 Oct 2022 23:49:07 -1000 Subject: [PATCH 213/231] feat(logging): log remote ip in output, add os_mon for monitoring --- config/config.exs | 2 +- lib/epochtalk_server_web/router.ex | 4 ---- mix.exs | 2 +- mix.lock | 15 ++++++++------- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/config/config.exs b/config/config.exs index 9bef11e6..da10a0b4 100644 --- a/config/config.exs +++ b/config/config.exs @@ -78,7 +78,7 @@ config :swoosh, :api_client, false # Configures Elixir's Logger config :logger, :console, format: "$time $metadata[$level] $message\n", - metadata: [:request_id] + metadata: [:request_id, :remote_ip] # Use Jason for JSON parsing in Phoenix config :phoenix, :json_library, Jason diff --git a/lib/epochtalk_server_web/router.ex b/lib/epochtalk_server_web/router.ex index 21de2e50..be0bee42 100644 --- a/lib/epochtalk_server_web/router.ex +++ b/lib/epochtalk_server_web/router.ex @@ -35,11 +35,7 @@ defmodule EpochtalkServerWeb.Router do # Note that preview only shows emails that were sent by the same # node running the Phoenix server. if Mix.env == :dev do - pipeline :browser do - plug :accepts, ["html"] - end scope "/dev" do - pipe_through [:browser] forward "/mailbox", Plug.Swoosh.MailboxPreview end end diff --git a/mix.exs b/mix.exs index 5cec6b8c..fda928fd 100644 --- a/mix.exs +++ b/mix.exs @@ -20,7 +20,7 @@ defmodule EpochtalkServer.MixProject do def application do [ mod: {EpochtalkServer.Application, []}, - extra_applications: [:logger, :runtime_tools] + extra_applications: [:logger, :runtime_tools, :os_mon] ] end diff --git a/mix.lock b/mix.lock index 6b871f1d..d9ceb466 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,6 @@ %{ "argon2_elixir": {:hex, :argon2_elixir, "3.0.0", "fd4405f593e77b525a5c667282172dd32772d7c4fa58cdecdaae79d2713b6c5f", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "8b753b270af557d51ba13fcdebc0f0ab27a2a6792df72fd5a6cf9cfaffcedc57"}, + "castore": {:hex, :castore, "0.1.18", "deb5b9ab02400561b6f5708f3e7660fc35ca2d51bfc6a940d2f513f89c2975fc", [:mix], [], "hexpm", "61bbaf6452b782ef80b33cdb45701afbcf0a918a45ebe7e73f1130d661e66a06"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "comeonin": {:hex, :comeonin, "5.3.3", "2c564dac95a35650e9b6acfe6d2952083d8a08e4a89b93a481acb552b325892e", [:mix], [], "hexpm", "3e38c9c2cb080828116597ca8807bb482618a315bfafd98c90bc22a821cc84df"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, @@ -9,13 +10,13 @@ "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, "db_connection": {:hex, :db_connection, "2.4.2", "f92e79aff2375299a16bcb069a14ee8615c3414863a6fef93156aee8e86c2ff3", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4fe53ca91b99f55ea249693a0229356a08f4d1a7931d8ffa79289b145fe83668"}, "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.27", "755da957e2b980618ba3397d3f923004d85bac244818cf92544eaa38585cb3a8", [:mix], [], "hexpm", "8d02465c243ee96bdd655e7c9a91817a2a80223d63743545b2861023c4ff39ac"}, - "ecto": {:hex, :ecto, "3.8.4", "e06b8b87e62b27fea17fd2ff6041572ddd10339fd16cdf58446e402c6c90a74b", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f9244288b8d42db40515463a008cf3f4e0e564bb9c249fe87bf28a6d79fe82d4"}, - "ecto_sql": {:hex, :ecto_sql, "3.8.3", "a7d22c624202546a39d615ed7a6b784580391e65723f2d24f65941b4dd73d471", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.8.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "348cb17fb9e6daf6f251a87049eafcb57805e2892e5e6a0f5dea0985d367329b"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.28", "0bf6546eb7cd6185ae086cbc5d20cd6dbb4b428aad14c02c49f7b554484b4586", [:mix], [], "hexpm", "501cef12286a3231dc80c81352a9453decf9586977f917a96e619293132743fb"}, + "ecto": {:hex, :ecto, "3.9.1", "67173b1687afeb68ce805ee7420b4261649d5e2deed8fe5550df23bab0bc4396", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c80bb3d736648df790f7f92f81b36c922d9dd3203ca65be4ff01d067f54eb304"}, + "ecto_sql": {:hex, :ecto_sql, "3.9.0", "2bb21210a2a13317e098a420a8c1cc58b0c3421ab8e3acfa96417dab7817918c", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a8f3f720073b8b1ac4c978be25fa7960ed7fd44997420c304a4a2e200b596453"}, "elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"}, - "ex_doc": {:hex, :ex_doc, "0.28.5", "3e52a6d2130ce74d096859e477b97080c156d0926701c13870a4e1f752363279", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "d2c4b07133113e9aa3e9ba27efb9088ba900e9e51caa383919676afdf09ab181"}, + "ex_doc": {:hex, :ex_doc, "0.28.6", "2bbd7a143d3014fc26de9056793e97600ae8978af2ced82c2575f130b7c0d7d7", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bca1441614654710ba37a0e173079273d619f9160cbcc8cd04e6bd59f1ad0e29"}, "gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"}, - "guardian": {:hex, :guardian, "2.2.4", "3dafdc19665411c96b2796d184064d691bc08813a132da5119e39302a252b755", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "6f83d4309c16ec2469da8606bb2a9815512cc2fac1595ad34b79940a224eb110"}, + "guardian": {:hex, :guardian, "2.3.0", "1e2a90e809fbd99439f5279db03fb30b7b2b2fc0d3870a0d76a84b099f1a2892", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "ced3ace74fc2b22b2b25dbe99e6d205443dd0d57d1cc25155a223b5e9656205e"}, "guardian_db": {:hex, :guardian_db, "2.1.0", "ec95a9d99cdd1e550555d09a7bb4a340d8887aad0697f594590c2fd74be02426", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:guardian, "~> 1.0 or ~> 2.0", [hex: :guardian, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "f8e7d543ac92c395f3a7fd5acbe6829faeade57d688f7562e2f0fca8f94a0d70"}, "guardian_redis": {:hex, :guardian_redis, "0.1.0", "705f8291346d813755a701e0241acaa845453d662aca675f1de8f0821554a6f9", [:mix], [{:guardian_db, "~> 2.0", [hex: :guardian_db, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:redix, "~> 1.0", [hex: :redix, repo: "hexpm", optional: false]}], "hexpm", "8058f6613f888a2f10836f6b669be3f21c7d303955862b638fc3c1bafaf66bef"}, "iteraptor": {:git, "https://github.com/epochtalk/elixir-iteraptor.git", "d8d1c386c38e06bdfcf60c9ce1abf8e49161cab4", [tag: "1.13.1"]}, @@ -26,14 +27,14 @@ "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"}, "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, - "phoenix": {:hex, :phoenix, "1.6.11", "29f3c0fd12fa1fc4d4b05e341578e55bc78d96ea83a022587a7e276884d397e4", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1664e34f80c25ea4918fbadd957f491225ef601c0e00b4e644b1a772864bfbc2"}, + "phoenix": {:hex, :phoenix, "1.6.14", "57678366dc1d5bad49832a0fc7f12c2830c10d3eacfad681bfe9602cd4445f04", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d48c0da00b3d4cd1aad6055387917491af9f6e1f1e96cedf6c6b7998df9dba26"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"}, "phoenix_view": {:hex, :phoenix_view, "1.1.2", "1b82764a065fb41051637872c7bd07ed2fdb6f5c3bd89684d4dca6e10115c95a", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "7ae90ad27b09091266f6adbb61e1d2516a7c3d7062c6789d46a7554ec40f3a56"}, "plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"}, "plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"}, "plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"}, - "postgrex": {:hex, :postgrex, "0.16.4", "26d998467b4a22252285e728a29d341e08403d084e44674784975bb1cd00d2cb", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "3234d1a70cb7b1e0c95d2e242785ec2a7a94a092bbcef4472320b950cfd64c5f"}, + "postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "redix": {:hex, :redix, "1.1.5", "6fc460d66a5c2287e83e6d73dddc8d527ff59cb4d4f298b41e03a4db8c3b2bd5", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "679afdd4c14502fe9c11387ff1cdcb33065a1cf511097da1eee407f17c7a418b"}, "remote_ip": {:hex, :remote_ip, "1.0.0", "3d7fb45204a5704443f480cee9515e464997f52c35e0a60b6ece1f81484067ae", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9e9fcad4e50c43b5234bb6a9629ed6ab223f3ed07147bd35470e4ee5c8caf907"}, From 9fba86d052a833aeafef34aa3e65216ea058ad2b Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 13 Oct 2022 23:52:42 -1000 Subject: [PATCH 214/231] feat(dialyzer): add dialyzer, fix all errors --- lib/epochtalk_server/models/ban.ex | 27 ++---- lib/epochtalk_server/models/banned_address.ex | 43 ++++------ lib/epochtalk_server/models/board.ex | 35 ++++---- lib/epochtalk_server/models/board_mapping.ex | 21 ++--- .../models/board_moderator.ex | 4 +- lib/epochtalk_server/models/category.ex | 41 ++++------ lib/epochtalk_server/models/invitation.ex | 19 ++--- lib/epochtalk_server/models/metadata_board.ex | 24 +++--- lib/epochtalk_server/models/permission.ex | 7 +- lib/epochtalk_server/models/preference.ex | 27 +++--- lib/epochtalk_server/models/profile.ex | 21 ++--- lib/epochtalk_server/models/role.ex | 53 ++++-------- .../models/role_permission.ex | 25 ++---- lib/epochtalk_server/models/role_user.ex | 13 +-- lib/epochtalk_server/models/user.ex | 82 +++++++------------ lib/epochtalk_server/session.ex | 18 ++-- .../controllers/user_controller.ex | 23 ++---- .../errors/custom_errors.ex | 30 ------- .../plugs/prepare_parse.ex | 4 +- mix.exs | 3 +- mix.lock | 2 + 21 files changed, 184 insertions(+), 338 deletions(-) diff --git a/lib/epochtalk_server/models/ban.ex b/lib/epochtalk_server/models/ban.ex index ebe992e9..d4790b56 100644 --- a/lib/epochtalk_server/models/ban.ex +++ b/lib/epochtalk_server/models/ban.ex @@ -14,10 +14,10 @@ defmodule EpochtalkServer.Models.Ban do @max_date ~N[9999-12-31 00:00:00.000] @type t :: %__MODULE__{ - user_id: non_neg_integer, - expiration: NaiveDateTime.t(), - created_at: NaiveDateTime.t(), - updated_at: NaiveDateTime.t() + user_id: non_neg_integer | nil, + expiration: NaiveDateTime.t() | nil, + created_at: NaiveDateTime.t() | nil, + updated_at: NaiveDateTime.t() | nil } @schema_prefix "users" @derive {Jason.Encoder, only: [:user_id, :expiration, :created_at, :updated_at]} @@ -33,10 +33,7 @@ defmodule EpochtalkServer.Models.Ban do @doc """ Create generic changeset for `Ban` model """ - @spec changeset( - ban :: t(), - attrs :: %{} | nil - ) :: t() + @spec changeset(ban :: t(), attrs :: map() | nil) :: %Ecto.Changeset{} def changeset(ban, attrs \\ %{}) do ban |> cast(attrs, [:id, :user_id, :expiration, :created_at, :updated_at]) @@ -46,10 +43,7 @@ defmodule EpochtalkServer.Models.Ban do @doc """ Create ban changeset for `Ban` model, handles upsert of ban for banning """ - @spec ban_changeset( - ban :: t(), - attrs :: %{} | nil - ) :: t() + @spec ban_changeset(ban :: t(), attrs :: map() | nil) :: %Ecto.Changeset{} def ban_changeset(ban, attrs \\ %{}) do now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) attrs = attrs @@ -64,10 +58,7 @@ defmodule EpochtalkServer.Models.Ban do @doc """ Create unban changeset for `Ban` model, handles update of ban for unbanning """ - @spec unban_changeset( - ban :: t(), - attrs :: %{} | nil - ) :: t() + @spec unban_changeset(ban :: t(), attrs :: map() | nil) :: %Ecto.Changeset{} def unban_changeset(ban, attrs \\ %{}) do now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) attrs = attrs @@ -167,9 +158,7 @@ defmodule EpochtalkServer.Models.Ban do @doc """ Used to unban a `User`. Updates supplied `User` model to reflect unbanning and returns. """ - @spec unban( - user :: User.t() - ) :: {:ok, user_changeset :: Ecto.Changeset.t()} | {:error, :unban_error} + @spec unban(user :: User.t()) :: {:ok, user :: User.t()} | {:error, :unban_error} def unban(%User{ban_info: %Ban{expiration: expiration}, id: user_id} = user) do if NaiveDateTime.compare(expiration, NaiveDateTime.utc_now) == :lt do case unban_by_user_id(user_id) do diff --git a/lib/epochtalk_server/models/banned_address.ex b/lib/epochtalk_server/models/banned_address.ex index 4b4a22ac..2bd2ed70 100644 --- a/lib/epochtalk_server/models/banned_address.ex +++ b/lib/epochtalk_server/models/banned_address.ex @@ -8,16 +8,16 @@ defmodule EpochtalkServer.Models.BannedAddress do `BannedAddress` model, for performing actions relating to banning by ip/hostname """ @type t :: %__MODULE__{ - hostname: String.t(), - ip1: non_neg_integer, - ip2: non_neg_integer, - ip3: non_neg_integer, - ip4: non_neg_integer, - weight: float, - decay: boolean, - created_at: NaiveDateTime.t(), - imported_at: NaiveDateTime.t(), - updates: [NaiveDateTime.t()] + hostname: String.t() | nil, + ip1: non_neg_integer | nil, + ip2: non_neg_integer | nil, + ip3: non_neg_integer | nil, + ip4: non_neg_integer | nil, + weight: float | nil, + decay: boolean | nil, + created_at: NaiveDateTime.t() | nil, + imported_at: NaiveDateTime.t() | nil, + updates: [NaiveDateTime.t()] | nil } @primary_key false schema "banned_addresses" do @@ -38,10 +38,7 @@ defmodule EpochtalkServer.Models.BannedAddress do @doc """ Creates changeset for upsert of `BannedAddress` model """ - @spec upsert_changeset( - banned_address :: t(), - attrs :: %{} | nil - ) :: t() + @spec upsert_changeset(banned_address :: t(), attrs :: map() | nil) :: %Ecto.Changeset{} def upsert_changeset(banned_address, attrs \\ %{}) do now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) attrs = attrs @@ -63,10 +60,7 @@ defmodule EpochtalkServer.Models.BannedAddress do @doc """ Creates changeset of `BannedAddress` model with hostname information """ - @spec hostname_changeset( - banned_address :: t(), - attrs :: %{} | nil - ) :: t() + @spec hostname_changeset(banned_address :: t(), attrs :: map() | nil) :: %Ecto.Changeset{} def hostname_changeset(banned_address, attrs \\ %{}) do banned_address |> cast(attrs, [:hostname, :weight, :decay, :imported_at, :created_at, :updates]) @@ -77,10 +71,7 @@ defmodule EpochtalkServer.Models.BannedAddress do @doc """ Creates changeset of `BannedAddress` model with IP information """ - @spec ip_changeset( - banned_address :: t(), - attrs :: %{} | nil - ) :: t() + @spec ip_changeset(banned_address :: t(), attrs :: map() | nil) :: %Ecto.Changeset{} def ip_changeset(banned_address, attrs \\ %{}) do cs_data = banned_address |> cast(attrs, [:ip1, :ip2, :ip3, :ip4, :weight, :decay, :imported_at, :created_at, :updates]) @@ -104,9 +95,7 @@ defmodule EpochtalkServer.Models.BannedAddress do @doc """ Upserts a `BannedAddress` into the database and handles calculation of weight accounting for decay """ - @spec upsert( - banned_address_or_list :: %{} | [%{}] | t() | [t()] - ) :: {:ok, banned_address_changeset :: Ecto.Changeset.t()} | {:error, :banned_address_error} + @spec upsert(banned_address_or_list :: map() | [map()] | t() | [t()]) :: {:ok, banned_address_changeset :: Ecto.Changeset.t()} | {:error, :banned_address_error} def upsert(address_list) when is_list(address_list) do Repo.transaction(fn -> Enum.each(address_list ,&upsert_one(&1)) end) |> case do @@ -162,9 +151,7 @@ defmodule EpochtalkServer.Models.BannedAddress do @doc """ Calculates the malicious score of the provided IP address, `float` score is returned if IP/Hostname are malicious, otherwise nil """ - @spec calculate_malicious_score_from_ip( - ip_address :: String.t() - ) :: float | nil + @spec calculate_malicious_score_from_ip(ip_address :: String.t()) :: float | nil def calculate_malicious_score_from_ip(ip) when is_binary(ip) do case :inet.parse_address(to_charlist(ip)) do {:ok, ip} -> diff --git a/lib/epochtalk_server/models/board.ex b/lib/epochtalk_server/models/board.ex index 8a22865c..c05bcd11 100644 --- a/lib/epochtalk_server/models/board.ex +++ b/lib/epochtalk_server/models/board.ex @@ -10,18 +10,18 @@ defmodule EpochtalkServer.Models.Board do `Board` model, for performing actions relating to forum boards """ @type t :: %__MODULE__{ - category: Category.t(), - name: String.t(), - slug: String.t(), - description: String.t(), - post_count: non_neg_integer, - thread_count: non_neg_integer, - viewable_by: non_neg_integer, - postable_by: non_neg_integer, - right_to_left: boolean, - created_at: NaiveDateTime.t(), - imported_at: NaiveDateTime.t(), - meta: %{} + category: Category.t() | term(), + name: String.t() | nil, + slug: String.t() | nil, + description: String.t() | nil, + post_count: non_neg_integer | nil, + thread_count: non_neg_integer | nil, + viewable_by: non_neg_integer | nil, + postable_by: non_neg_integer | nil, + right_to_left: boolean | nil, + created_at: NaiveDateTime.t() | nil, + imported_at: NaiveDateTime.t() | nil, + meta: map() | nil } schema "boards" do field :name, :string @@ -46,8 +46,8 @@ defmodule EpochtalkServer.Models.Board do """ @spec changeset( board :: t(), - attrs :: %{} | nil - ) :: t() + attrs :: map() | nil + ) :: %Ecto.Changeset{} def changeset(board, attrs) do board |> cast(attrs, [:id, :name, :slug, :description, :post_count, :thread_count, :viewable_by, :postable_by, :created_at, :imported_at, :updated_at, :meta]) @@ -61,10 +61,7 @@ defmodule EpochtalkServer.Models.Board do @doc """ Create changeset for creation of `Board` model """ - @spec create_changeset( - board :: t(), - attrs :: %{} | nil - ) :: t() + @spec create_changeset(board :: t(), attrs :: map() | nil) :: %Ecto.Changeset{} def create_changeset(board, attrs) do now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) attrs = attrs @@ -82,7 +79,7 @@ defmodule EpochtalkServer.Models.Board do Creates a new `Board` in the database """ @spec create( - board_attrs :: %{} + board_attrs :: map() ) :: {:ok, board :: t()} | {:error, Ecto.Changeset.t()} def create(board) do board_cs = create_changeset(%Board{}, board) diff --git a/lib/epochtalk_server/models/board_mapping.ex b/lib/epochtalk_server/models/board_mapping.ex index b2d79464..32a9e46a 100644 --- a/lib/epochtalk_server/models/board_mapping.ex +++ b/lib/epochtalk_server/models/board_mapping.ex @@ -10,10 +10,10 @@ defmodule EpochtalkServer.Models.BoardMapping do `BoardMapping` model, for performing actions relating to mapping forum boards and categories """ @type t :: %__MODULE__{ - board: Board.t(), - parent: Board.t(), - category: Category.t(), - view_order: non_neg_integer + board: Board.t() | term(), + parent: Board.t() | term(), + category: Category.t() | term(), + view_order: non_neg_integer | nil } @primary_key false schema "board_mapping" do @@ -28,10 +28,7 @@ defmodule EpochtalkServer.Models.BoardMapping do @doc """ Create generic changeset for `BoardMapping` model """ - @spec changeset( - board_mapping :: t(), - attrs :: %{} | nil - ) :: t() + @spec changeset(board_mapping :: t(), attrs :: map() | nil) :: %Ecto.Changeset{} def changeset(board_mapping, attrs) do board_mapping |> cast(attrs, [:board_id, :parent_id, :category_id, :view_order]) @@ -45,9 +42,7 @@ defmodule EpochtalkServer.Models.BoardMapping do @doc """ Deletes a `Board` from the `BoardMapping` """ - @spec delete_board_by_id( - board_id :: integer() - ) :: {non_neg_integer(), nil | [term()]} + @spec delete_board_by_id(board_id :: integer()) :: {non_neg_integer(), nil | [term()]} def delete_board_by_id(id) when is_integer(id) do query = from bm in BoardMapping, where: bm.board_id == ^id @@ -57,9 +52,7 @@ defmodule EpochtalkServer.Models.BoardMapping do @doc """ Updates `BoardMapping` in the database """ - @spec update( - board_mapping_list :: [%{}] - ) :: {:ok, Ecto.Changeset.t()} | {:error, Ecto.Changeset.t()}| {:error, any()} + @spec update(board_mapping_list :: [%{}]) :: {:ok, Ecto.Changeset.t()} | {:error, Ecto.Changeset.t()}| {:error, any()} def update(board_mapping_list) do Repo.transaction(fn -> Enum.each(board_mapping_list, &update(&1, Map.get(&1, :type))) diff --git a/lib/epochtalk_server/models/board_moderator.ex b/lib/epochtalk_server/models/board_moderator.ex index 6c450fd3..d466ab90 100644 --- a/lib/epochtalk_server/models/board_moderator.ex +++ b/lib/epochtalk_server/models/board_moderator.ex @@ -23,8 +23,8 @@ defmodule EpochtalkServer.Models.BoardModerator do """ @spec changeset( board_moderator :: t(), - attrs :: %{} | nil - ) :: Board.t() + attrs :: map() | nil + ) :: %Ecto.Changeset{} def changeset(board_moderator, attrs \\ %{}) do board_moderator |> cast(attrs, [:user_id, :board_id]) diff --git a/lib/epochtalk_server/models/category.ex b/lib/epochtalk_server/models/category.ex index 25334627..91efa9ae 100644 --- a/lib/epochtalk_server/models/category.ex +++ b/lib/epochtalk_server/models/category.ex @@ -9,15 +9,15 @@ defmodule EpochtalkServer.Models.Category do `Category` model, for performing actions relating to forum categories """ @type t :: %__MODULE__{ - name: String.t(), - view_order: non_neg_integer, - viewable_by: non_neg_integer, - postable_by: non_neg_integer, - created_at: NaiveDateTime.t(), - imported_at: NaiveDateTime.t(), - updated_at: NaiveDateTime.t(), - meta: %{}, - boards: [Board.t()] + name: String.t() | nil, + view_order: non_neg_integer | nil, + viewable_by: non_neg_integer | nil, + postable_by: non_neg_integer | nil, + created_at: NaiveDateTime.t() | nil, + imported_at: NaiveDateTime.t() | nil, + updated_at: NaiveDateTime.t() | nil, + meta: map() | nil, + boards: [Board.t()] | term() } schema "categories" do field :name, :string @@ -36,10 +36,7 @@ defmodule EpochtalkServer.Models.Category do @doc """ Create generic changeset for `Category` model """ - @spec changeset( - category :: t(), - attrs :: %{} | nil - ) :: t() + @spec changeset(category :: t(), attrs :: map() | nil) :: %Ecto.Changeset{} def changeset(category, attrs) do category |> cast(attrs, [:id, :name, :view_order, :viewable_by, :postable_by, :created_at, :imported_at, :updated_at, :meta]) @@ -49,10 +46,7 @@ defmodule EpochtalkServer.Models.Category do @doc """ Creates changeset for inserting a new `Category` model """ - @spec create_changeset( - category :: t(), - attrs :: %{} | nil - ) :: t() + @spec create_changeset(category :: t(), attrs :: map() | nil) :: %Ecto.Changeset{} def create_changeset(category, attrs) do now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) attrs = attrs @@ -65,10 +59,7 @@ defmodule EpochtalkServer.Models.Category do @doc """ Creates changeset for updating an existing `Category` model """ - @spec create_changeset( - category :: t(), - attrs :: %{} | nil - ) :: t() + @spec update_for_board_mapping_changeset(category :: t(), attrs :: map() | nil) :: %Ecto.Changeset{} def update_for_board_mapping_changeset(category, attrs) do category |> cast(attrs, [:id, :name, :view_order, :viewable_by]) @@ -80,9 +71,7 @@ defmodule EpochtalkServer.Models.Category do @doc """ Creates a new `Category` in the database """ - @spec create( - category_attrs :: %{} - ) :: {:ok, category :: t()} | {:error, Ecto.Changeset.t()} + @spec create(category_attrs :: map()) :: {:ok, category :: t()} | {:error, Ecto.Changeset.t()} def create(attrs) do category_cs = create_changeset(%Category{}, attrs) Repo.insert(category_cs) @@ -91,9 +80,7 @@ defmodule EpochtalkServer.Models.Category do @doc """ Updates an existing `Category` in the database, used by board mapping to recategorize boards """ - @spec update_for_board_mapping( - category_map :: %{ id: id :: integer } - ) :: {:ok, category :: t()} | {:error, Ecto.Changeset.t()} + @spec update_for_board_mapping(category_map :: %{ id: id :: integer }) :: {:ok, category :: t()} | {:error, Ecto.Changeset.t()} def update_for_board_mapping(%{ id: id } = category_map) do %Category{ id: id } |> update_for_board_mapping_changeset(category_map) diff --git a/lib/epochtalk_server/models/invitation.ex b/lib/epochtalk_server/models/invitation.ex index 706a38bf..50f2ed74 100644 --- a/lib/epochtalk_server/models/invitation.ex +++ b/lib/epochtalk_server/models/invitation.ex @@ -9,9 +9,9 @@ defmodule EpochtalkServer.Models.Invitation do """ @type t :: %__MODULE__{ - email: String.t(), - hash: String.t(), - created_at: NaiveDateTime.t() + email: String.t() | nil, + hash: String.t() | nil, + created_at: NaiveDateTime.t() | nil } @primary_key false schema "invitations" do @@ -25,10 +25,7 @@ defmodule EpochtalkServer.Models.Invitation do @doc """ Create changeset for inserting a new `Invitation` model """ - @spec create_changeset( - invitation :: t(), - attrs :: %{} | nil - ) :: t() + @spec create_changeset(invitation :: t(), attrs :: map() | nil) :: %Ecto.Changeset{} def create_changeset(invitation, attrs \\ %{}) do now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) attrs = attrs @@ -49,16 +46,12 @@ defmodule EpochtalkServer.Models.Invitation do @doc """ Creates a new `Invitation` in the database """ - @spec create( - email :: String.t() - ) :: {:ok, invitation :: t()} | {:error, Ecto.Changeset.t()} + @spec create(email :: String.t()) :: {:ok, invitation :: t()} | {:error, Ecto.Changeset.t()} def create(email), do: Repo.insert(create_changeset(%Invitation{}, %{email: email})) @doc """ Deletes `Invitation` from the database by email """ - @spec delete( - email :: String.t() - ) :: {non_neg_integer(), nil | [term()]} + @spec delete(email :: String.t()) :: {non_neg_integer(), nil | [term()]} def delete(email), do: Repo.delete_all(from(i in Invitation, where: i.email == ^email)) end diff --git a/lib/epochtalk_server/models/metadata_board.ex b/lib/epochtalk_server/models/metadata_board.ex index 40da3f3a..ca61ec11 100644 --- a/lib/epochtalk_server/models/metadata_board.ex +++ b/lib/epochtalk_server/models/metadata_board.ex @@ -9,16 +9,16 @@ defmodule EpochtalkServer.Models.MetadataBoard do """ @type t :: %__MODULE__{ - board: Board.t(), - post_count: non_neg_integer, - thread_count: non_neg_integer, - total_post: non_neg_integer, - total_thread_count: non_neg_integer, - last_post_username: String.t(), - last_post_created_at: NaiveDateTime.t(), - last_thread_id: non_neg_integer, - last_thread_title: String.t(), - last_post_position: non_neg_integer + board: Board.t() | term(), + post_count: non_neg_integer | nil, + thread_count: non_neg_integer | nil, + total_post: non_neg_integer | nil, + total_thread_count: non_neg_integer | nil, + last_post_username: String.t() | nil, + last_post_created_at: NaiveDateTime.t() | nil, + last_thread_id: non_neg_integer | nil, + last_thread_title: String.t() | nil, + last_post_position: non_neg_integer | nil } @schema_prefix "metadata" schema "boards" do @@ -41,8 +41,8 @@ defmodule EpochtalkServer.Models.MetadataBoard do """ @spec changeset( metadata_board :: t(), - attrs :: %{} | nil - ) :: t() + attrs :: map() | nil + ) :: %Ecto.Changeset{} def changeset(metadata_board, attrs \\ %{}) do metadata_board |> cast(attrs, [:id, :board_id, :post_count, :thread_count, diff --git a/lib/epochtalk_server/models/permission.ex b/lib/epochtalk_server/models/permission.ex index 8ef51cd0..d4789f8e 100644 --- a/lib/epochtalk_server/models/permission.ex +++ b/lib/epochtalk_server/models/permission.ex @@ -8,7 +8,7 @@ defmodule EpochtalkServer.Models.Permission do """ @type t :: %__MODULE__{ - path: String.t() + path: String.t() | nil } @primary_key false schema "permissions" do @@ -20,10 +20,7 @@ defmodule EpochtalkServer.Models.Permission do @doc """ Creates a generic changeset for `Permission` model """ - @spec changeset( - permission :: t(), - attrs :: %{} | nil - ) :: t() + @spec changeset(permission :: t(), attrs :: map() | nil) :: %Ecto.Changeset{} def changeset(permission, attrs \\ %{}) do permission |> cast(attrs, [:path]) diff --git a/lib/epochtalk_server/models/preference.ex b/lib/epochtalk_server/models/preference.ex index 041889b4..a19d574b 100644 --- a/lib/epochtalk_server/models/preference.ex +++ b/lib/epochtalk_server/models/preference.ex @@ -6,17 +6,17 @@ defmodule EpochtalkServer.Models.Preference do `Preference` model, for performing actions relating to a user's preferences """ @type t :: %__MODULE__{ - user_id: non_neg_integer, - posts_per_page: non_neg_integer, - threads_per_page: non_neg_integer, - collapsed_categories: %{}, - ignored_boards: %{}, - timezone_offset: String.t(), - notify_replied_threads: boolean, - ignore_newbies: boolean, - patroller_view: boolean, - email_mentions: boolean, - email_messages: boolean + user_id: non_neg_integer | nil, + posts_per_page: non_neg_integer | nil, + threads_per_page: non_neg_integer | nil, + collapsed_categories: %{} | nil, + ignored_boards: %{} | nil, + timezone_offset: String.t() | nil, + notify_replied_threads: boolean | nil, + ignore_newbies: boolean | nil, + patroller_view: boolean | nil, + email_mentions: boolean | nil, + email_messages: boolean | nil } @primary_key false @schema_prefix "users" @@ -39,10 +39,7 @@ defmodule EpochtalkServer.Models.Preference do @doc """ Creates a generic changeset for `Preference` model """ - @spec changeset( - preference :: t(), - attrs :: %{} | nil - ) :: t() + @spec changeset(preference :: t(), attrs :: map() | nil) :: %Ecto.Changeset{} def changeset(preference, attrs \\ %{}) do preference |> cast(attrs, [:user_id, :posts_per_page, :threads_per_page, diff --git a/lib/epochtalk_server/models/profile.ex b/lib/epochtalk_server/models/profile.ex index 1aba077a..630d5161 100644 --- a/lib/epochtalk_server/models/profile.ex +++ b/lib/epochtalk_server/models/profile.ex @@ -6,14 +6,14 @@ defmodule EpochtalkServer.Models.Profile do `Profile` model, for performing actions relating a user's profile """ @type t :: %__MODULE__{ - user_id: non_neg_integer, - avatar: String.t(), - position: String.t(), - signature: String.t(), - raw_signature: String.t(), - post_count: non_neg_integer, - fields: %{}, - last_active: NaiveDateTime.t() + user_id: non_neg_integer | nil, + avatar: String.t() | nil, + position: String.t() | nil, + signature: String.t() | nil, + raw_signature: String.t() | nil, + post_count: non_neg_integer | nil, + fields: map() | nil, + last_active: NaiveDateTime.t() | nil } @schema_prefix "users" schema "profiles" do @@ -32,10 +32,7 @@ defmodule EpochtalkServer.Models.Profile do @doc """ Creates a generic changeset for `Profile` model """ - @spec changeset( - profile :: t(), - attrs :: %{} | nil - ) :: t() + @spec changeset(profile :: t(), attrs :: map() | nil) :: %Ecto.Changeset{} def changeset(profile, attrs \\ %{}) do profile |> cast(attrs, [:user_id, :avatar, :position, :signature, :raw_signature, :post_count, :field, :last_active]) diff --git a/lib/epochtalk_server/models/role.ex b/lib/epochtalk_server/models/role.ex index 32740873..f0cf8c25 100644 --- a/lib/epochtalk_server/models/role.ex +++ b/lib/epochtalk_server/models/role.ex @@ -10,15 +10,15 @@ defmodule EpochtalkServer.Models.Role do `Role` model, for performing actions relating to user roles """ @type t :: %__MODULE__{ - name: String.t(), - description: String.t(), - lookup: String.t(), - priority: non_neg_integer, - highlight_color: String.t(), - permissions: %{}, - priority_restrictions: [non_neg_integer], - created_at: NaiveDateTime.t(), - updated_at: NaiveDateTime.t() + name: String.t() | nil, + description: String.t() | nil, + lookup: String.t() | nil, + priority: non_neg_integer | nil, + highlight_color: String.t() | nil, + permissions: map() | nil, + priority_restrictions: [non_neg_integer] | nil, + created_at: NaiveDateTime.t() | nil, + updated_at: NaiveDateTime.t() | nil } @derive {Jason.Encoder, only: [:name, :description, :lookup, :priority, :highlight_color, :permissions, :priority_restrictions, :created_at, :updated_at]} schema "roles" do @@ -40,10 +40,7 @@ defmodule EpochtalkServer.Models.Role do @doc """ Create generic changeset for the `Role` model """ - @spec changeset( - role :: t(), - attrs :: %{} | nil - ) :: t() + @spec changeset(role :: t(), attrs :: map() | nil) :: %Ecto.Changeset{} def changeset(role, attrs \\ %{}) do role |> cast(attrs, [:name, :description, :lookup, :priority, :permissions]) @@ -81,9 +78,7 @@ defmodule EpochtalkServer.Models.Role do @doc """ Returns a `Role` or list of roles, for specified lookup(s) """ - @spec by_lookup( - lookup_or_lookups :: String.t() | [String.t()] - ) :: t() | [t()] | [] | nil + @spec by_lookup(lookup_or_lookups :: String.t() | [String.t()]) :: t() | [t()] | [] | nil def by_lookup(lookups) when is_list(lookups) do from(r in Role, where: r.lookup in ^lookups) |> Repo.all end @@ -92,9 +87,7 @@ defmodule EpochtalkServer.Models.Role do @doc """ Returns a list containing a user's roles """ - @spec by_user_id( - user_id :: integer - ) :: [t()] + @spec by_user_id(user_id :: integer) :: [t()] def by_user_id(user_id) do query = from ru in RoleUser, join: r in Role, @@ -115,9 +108,7 @@ defmodule EpochtalkServer.Models.Role do @doc """ Inserts a new `Role` into the database """ - @spec insert( - role_or_roles :: t() | [%{}] - ) :: {:ok, role :: t()} | {non_neg_integer(), nil | [term()]} | {:error, Ecto.Changeset.t()} + @spec insert(role_or_roles :: t() | [%{}]) :: {:ok, role :: t()} | {non_neg_integer(), nil | [term()]} | {:error, Ecto.Changeset.t()} def insert([]), do: {:error, "Role list is empty"} def insert(%Role{} = role), do: Repo.insert(role) def insert([%{}|_] = roles), do: Repo.insert_all(Role, roles) @@ -127,10 +118,7 @@ defmodule EpochtalkServer.Models.Role do @doc """ Updates the permissions of an existing `Role` in the database """ - @spec set_permissions( - id :: integer, - permissions_attrs :: %{} - ) :: {:ok, role :: t()} | {:error, Ecto.Changeset.t()} + @spec set_permissions(id :: integer, permissions_attrs :: map()) :: {:ok, role :: t()} | {:error, Ecto.Changeset.t()} def set_permissions(id, permissions) do Role |> Repo.get(id) @@ -149,9 +137,7 @@ defmodule EpochtalkServer.Models.Role do This helper needs to be called anywhere that modifies a user's roles and is expected to return the updated user's roles. """ - @spec handle_empty_user_roles( - user :: User.t() - ) :: User.t() + @spec handle_empty_user_roles(user :: User.t()) :: User.t() def handle_empty_user_roles(%User{roles: [%Role{} | _]} = user), do: user def handle_empty_user_roles(%User{roles: []} = user), do: user |> Map.put(:roles, [Role.get_default()]) def handle_empty_user_roles(%User{} = user), do: user |> Map.put(:roles, [Role.get_default()]) @@ -163,9 +149,7 @@ defmodule EpochtalkServer.Models.Role do This helper needs to be called anywhere that modifies a user's ban and is expected to return the updated user's roles. """ - @spec handle_banned_user_role( - user_or_roles :: User.t() | [t()] - ) :: User.t() | [t()] + @spec handle_banned_user_role(user_or_roles :: User.t() | [t()]) :: User.t() | [t()] # called with user model, outputs user model with updated role def handle_banned_user_role(%User{roles: [%Role{} | _] = roles} = user) do if banned_role = Enum.find(roles, &(&1.lookup == "banned")), @@ -179,10 +163,7 @@ defmodule EpochtalkServer.Models.Role do @doc """ Takes in list of user's roles, and returns an xored map of all `Role` permissions """ - @spec get_masked_permissions( - roles :: [t()] - ) :: %{} - # Given %User{}.roles, outputs xored permissions + @spec get_masked_permissions(roles :: [t()]) :: map() def get_masked_permissions(roles) when is_list(roles), do: Enum.reduce(roles, %{}, &mask_permissions(&2, &1)) ## === Private Helper Functions === diff --git a/lib/epochtalk_server/models/role_permission.ex b/lib/epochtalk_server/models/role_permission.ex index 8f82d739..8ec8d3b9 100644 --- a/lib/epochtalk_server/models/role_permission.ex +++ b/lib/epochtalk_server/models/role_permission.ex @@ -11,10 +11,10 @@ defmodule EpochtalkServer.Models.RolePermission do """ @type t :: %__MODULE__{ - role: Role.t(), - permission: Permission.t(), - value: boolean, - modified: boolean + role: Role.t() | term(), + permission: Permission.t() | term(), + value: boolean | nil, + modified: boolean | nil } @primary_key false schema "roles_permissions" do @@ -32,10 +32,7 @@ defmodule EpochtalkServer.Models.RolePermission do @doc """ Creates a generic changeset for `RolePermission` model """ - @spec changeset( - role_permission :: t(), - attrs :: %{} | nil - ) :: t() + @spec changeset(role_permission :: t(), attrs :: map() | nil) :: %Ecto.Changeset{} def changeset(role_permission, attrs \\ %{}) do role_permission |> cast(attrs, [:role_id, :permission_path, :value, :modified]) @@ -47,9 +44,7 @@ defmodule EpochtalkServer.Models.RolePermission do @doc """ Inserts a new `RolePermission` into the database """ - @spec insert( - role_permission_or_role_permissions :: t() | [%{}] - ) :: {:ok, role :: t()} | {non_neg_integer(), nil | [term()]} | {:error, Ecto.Changeset.t()} + @spec insert(role_permission_or_role_permissions :: t() | [map()]) :: {:ok, role :: t()} | {non_neg_integer(), nil | [term()]} | {:error, Ecto.Changeset.t()} def insert([]), do: {:error, "Role permission list is empty"} def insert(%RolePermission{} = role_permission), do: Repo.insert(role_permission) def insert([%{}|_] = roles_permissions), do: Repo.insert_all(RolePermission, roles_permissions) @@ -77,9 +72,7 @@ defmodule EpochtalkServer.Models.RolePermission do @doc """ Used to update the value of a `RolePermission` in the database, if it exists or created it, if it doesnt """ - @spec upsert_value( - role_permissions :: [%{}] - ) :: {non_neg_integer(), nil | [term()]} + @spec upsert_value(role_permissions :: [%{}]) :: {non_neg_integer(), nil | [term()]} # change the default values of roles permissions def upsert_value([]), do: {:error, "Role permission list is empty"} def upsert_value([%{}|_] = roles_permissions) do @@ -94,9 +87,7 @@ defmodule EpochtalkServer.Models.RolePermission do @doc """ Derives a single nested map of all permissions for a role """ - @spec permissions_map_by_role_id( - role_id :: integer - ) :: %{} + @spec permissions_map_by_role_id(role_id :: integer) :: map() def permissions_map_by_role_id(role_id) do from(rp in RolePermission, where: rp.role_id == ^role_id) diff --git a/lib/epochtalk_server/models/role_user.ex b/lib/epochtalk_server/models/role_user.ex index 2fbbe9b6..b3fa381b 100644 --- a/lib/epochtalk_server/models/role_user.ex +++ b/lib/epochtalk_server/models/role_user.ex @@ -13,8 +13,8 @@ defmodule EpochtalkServer.Models.RoleUser do @admin_role_id 1 @type t :: %__MODULE__{ - user: User.t(), - role: Role.t() + user: User.t() | term(), + role: Role.t() | term() } @primary_key false schema "roles_users" do @@ -27,10 +27,7 @@ defmodule EpochtalkServer.Models.RoleUser do @doc """ Creates a generic changeset for `RoleUser` model """ - @spec changeset( - role_user :: t(), - attrs :: %{} | nil - ) :: t() + @spec changeset(role_user :: t(), attrs :: map() | nil) :: %Ecto.Changeset{} def changeset(role_user, attrs \\ %{}) do role_user |> cast(attrs, [:user_id, :role_id]) @@ -42,9 +39,7 @@ defmodule EpochtalkServer.Models.RoleUser do @doc """ Assigns a specific `User` to have the `superAdministrator` `Role` """ - @spec set_admin( - user_id :: integer - ) :: {:ok, role_user :: t()} | {:error, Ecto.Changeset.t()} + @spec set_admin(user_id :: integer) :: {:ok, role_user :: t()} | {:error, Ecto.Changeset.t()} def set_admin(user_id), do: set_user_role(@admin_role_id, user_id) @doc """ diff --git a/lib/epochtalk_server/models/user.ex b/lib/epochtalk_server/models/user.ex index 6b00af30..dca4a744 100644 --- a/lib/epochtalk_server/models/user.ex +++ b/lib/epochtalk_server/models/user.ex @@ -15,25 +15,25 @@ defmodule EpochtalkServer.Models.User do `User` model, for performing actions relating a user """ @type t :: %__MODULE__{ - id: non_neg_integer, - email: String.t(), - username: String.t(), - password: String.t(), - password_confirmation: String.t(), - passhash: String.t(), - confirmation_token: String.t(), - reset_token: String.t(), - reset_expiration: String.t(), - deleted: boolean, - malicious_score: float, - created_at: NaiveDateTime.t(), - imported_at: NaiveDateTime.t(), - updated_at: NaiveDateTime.t(), - preferences: Preference.t(), - profile: Profile.t(), - ban_info: Ban.t(), - roles: [Role.t()], - moderating: [BoardModerator.t()] + id: non_neg_integer | nil, + email: String.t() | nil, + username: String.t() | nil, + password: String.t() | nil, + password_confirmation: String.t() | nil, + passhash: String.t() | nil, + confirmation_token: String.t() | nil, + reset_token: String.t() | nil, + reset_expiration: String.t() | nil, + deleted: boolean | nil, + malicious_score: float | nil, + created_at: NaiveDateTime.t() | nil, + imported_at: NaiveDateTime.t() | nil, + updated_at: NaiveDateTime.t() | nil, + preferences: Preference.t() | term(), + profile: Profile.t() | term(), + ban_info: Ban.t() | term(), + roles: [Role.t()] | term(), + moderating: [BoardModerator.t()] | term() } schema "users" do field :email, :string @@ -65,10 +65,7 @@ defmodule EpochtalkServer.Models.User do Creates a registration changeset for `User` model, returns an error changeset if validation of username, email and password do not pass. """ - @spec registration_changeset( - user :: t(), - attrs :: %{} | nil - ) :: t() + @spec registration_changeset(user :: t(), attrs :: map() | nil) :: %Ecto.Changeset{} def registration_changeset(user, attrs) do user |> cast(attrs, [:id, :email, :username, :created_at, :updated_at, :deleted, :malicious_score, :password]) @@ -83,9 +80,7 @@ defmodule EpochtalkServer.Models.User do @doc """ Creates a new `User` in the database, used for registration """ - @spec create( - attrs :: %{} - ) :: {:ok, user :: t()} | {:error, Ecto.Changeset.t()} + @spec create(attrs :: map()) :: {:ok, user :: t()} | {:error, Ecto.Changeset.t()} def create(attrs) do user_cs = User.registration_changeset(%User{}, attrs) case Repo.insert(user_cs) do @@ -101,10 +96,7 @@ defmodule EpochtalkServer.Models.User do @doc """ Creates a new `User` in the database and assigns the `superAdministrator` `Role`, used for seeding """ - @spec create( - attrs :: %{}, - admin :: boolean - ) :: {:ok, user :: t()} | {:error, Ecto.Changeset.t()} + @spec create(attrs :: map(), admin :: boolean) :: {:ok, user :: t()} | {:error, Ecto.Changeset.t()} def create(attrs, true = _admin) do Repo.transaction(fn -> {:ok, user} = create(attrs) @@ -115,33 +107,25 @@ defmodule EpochtalkServer.Models.User do @doc """ Checks if `User` with `username` exists in the database """ - @spec with_username_exists?( - username :: String.t() - ) :: true | false + @spec with_username_exists?(username :: String.t()) :: true | false def with_username_exists?(username), do: Repo.exists?(from u in User, where: u.username == ^username) @doc """ Checks if `User` with `email` exists in the database """ - @spec with_email_exists?( - email :: String.t() - ) :: true | false + @spec with_email_exists?(email :: String.t()) :: true | false def with_email_exists?(email), do: Repo.exists?(from u in User, where: u.email == ^email) @doc """ Gets a `User` from the database by `id` """ - @spec by_id( - id :: integer - ) :: t() | nil + @spec by_id(id :: integer) :: t() | nil def by_id(id) when is_integer(id), do: Repo.get_by(User, id: id) @doc """ Clears the malicious score of a `User` by `id`, from the database """ - @spec clear_malicious_score_by_id( - id :: integer - ) :: {non_neg_integer(), nil} + @spec clear_malicious_score_by_id(id :: integer) :: {non_neg_integer(), nil} def clear_malicious_score_by_id(id), do: set_malicious_score_by_id(id, nil) @doc """ @@ -149,9 +133,7 @@ defmodule EpochtalkServer.Models.User do Appends the `user` `Role` to `user.roles` if no roles present. Strips all roles but `banned` from `user.roles` if user is banned. """ - @spec by_username( - username :: String.t() - ) :: {:ok, user :: t()} | {:error, :user_not_found} + @spec by_username(username :: String.t()) :: {:ok, user :: t()} | {:error, :user_not_found} def by_username(username) when is_binary(username) do query = from u in User, where: u.username == ^username, @@ -167,10 +149,7 @@ defmodule EpochtalkServer.Models.User do `malicious_score` is updated and is assigned the `banned` `Role`, in the database and in place. Otherwise the user is just returned with no change. """ - @spec handle_malicious_user( - user :: t(), - ip :: tuple - ) :: {:ok, user :: t()} | {:error, :ban_error} + @spec handle_malicious_user(user :: t(), ip :: tuple) :: {:ok, user :: t()} | {:error, :ban_error} def handle_malicious_user(%User{} = user, ip) do # convert ip tuple into string ip_str = ip |> :inet_parse.ntoa |> to_string @@ -189,10 +168,7 @@ defmodule EpochtalkServer.Models.User do @doc """ Validates with Argon2 that a `User` `passhash` matches the supplied `password` """ - @spec valid_password?( - user :: t(), - password :: String.t() - ) :: true | false + @spec valid_password?(user :: t(), password :: String.t()) :: true | false def valid_password?(%User{passhash: hashed_password} = _user, password) when is_binary(hashed_password) and byte_size(password) > 0 do Argon2.verify_pass(password, hashed_password) diff --git a/lib/epochtalk_server/session.ex b/lib/epochtalk_server/session.ex index ec189ff8..08fe6575 100644 --- a/lib/epochtalk_server/session.ex +++ b/lib/epochtalk_server/session.ex @@ -72,16 +72,14 @@ defmodule EpochtalkServer.Session do @doc """ Deletes every session instance for the specified `User` """ - @spec delete_sessions( - user :: User.t(), - session_id :: String.t() - ) :: {:ok, Redix.Protocol.redis_value()} | {:error, atom() | Redix.Error.t() | Redix.ConnectionError.t()} - def delete_sessions(%User{} = user, session_id) do + @spec delete_sessions(user :: User.t()) :: :ok + def delete_sessions(%User{} = user) do # delete session id from redis under "user:{user_id}:sessions" session_key = generate_key(user.id, "sessions") - Redix.command(:redix, ["SPOP", session_key, session_id]) - # repeat until redix returns nil - |> unless do delete_sessions(user.id, session_id) end + case Redix.command(:redix, ["SPOP", session_key]) do + {:ok, nil} -> :ok + _ -> delete_sessions(user) # repeat until redix returns nil + end end defp save(%User{} = user, session_id) do @@ -102,7 +100,7 @@ defmodule EpochtalkServer.Session do role_lookups = roles |> Enum.map(&(&1.lookup)) role_key = generate_key(user_id, "roles") Redix.command(:redix, ["DEL", role_key]) - unless role_lookups == nil or role_lookups == [], do: + unless role_lookups == [], do: Enum.each(role_lookups, &Redix.command(:redix, ["SADD", role_key, &1])) end @@ -112,7 +110,7 @@ defmodule EpochtalkServer.Session do # save/replace moderating boards to redis under "user:{user_id}:moderating" moderating_key = generate_key(user_id, "moderating") Redix.command(:redix, ["DEL", moderating_key]) - unless moderating == nil or moderating == [], do: + unless moderating == [], do: Enum.each(moderating, &Redix.command(:redix, ["SADD", moderating_key, &1])) end diff --git a/lib/epochtalk_server_web/controllers/user_controller.ex b/lib/epochtalk_server_web/controllers/user_controller.ex index dc6d4477..cba34da2 100644 --- a/lib/epochtalk_server_web/controllers/user_controller.ex +++ b/lib/epochtalk_server_web/controllers/user_controller.ex @@ -9,13 +9,7 @@ defmodule EpochtalkServerWeb.UserController do alias EpochtalkServer.Auth.Guardian alias EpochtalkServer.Session alias EpochtalkServerWeb.ErrorHelpers - alias EpochtalkServerWeb.CustomErrors.{ - InvalidCredentials, - NotLoggedIn, - AccountNotConfirmed, - AccountMigrationNotComplete, - InvalidPayload - } + alias EpochtalkServerWeb.CustomErrors.InvalidPayload @doc """ Used to check if a username has already been taken @@ -74,7 +68,7 @@ defmodule EpochtalkServerWeb.UserController do conn <- Guardian.Plug.sign_out(conn) do render(conn, "logout.json", data: %{success: true}) else - {:auth, false} -> raise(NotLoggedIn) + {:auth, false} -> ErrorHelpers.render_json_error(conn, 400, "Not logged in") _ -> ErrorHelpers.render_json_error(conn, 500, "There was an issue signing out") end end @@ -89,18 +83,19 @@ defmodule EpochtalkServerWeb.UserController do {:not_confirmed, false} <- {:not_confirmed, !!Map.get(user, :confirmation_token)}, {:missing_passhash, true} <- {:missing_passhash, !!Map.get(user, :passhash)}, {:valid_password, true} <- {:valid_password, User.valid_password?(user, password)}, - {:ok, user} = Ban.unban(user), - {:ok, user, token, conn} = Session.create(user, remember_me, conn)do + {:ok, user} <- Ban.unban(user), + {:ok, user, token, conn} <- Session.create(user, remember_me, conn) do render(conn, "user.json", %{ user: user, token: token}) else {:auth, true} -> ErrorHelpers.render_json_error(conn, 400, "Already logged in") - {:error, :user_not_found} -> raise(InvalidCredentials) - {:not_confirmed, true} -> raise(AccountNotConfirmed) - {:missing_passhash, false} -> raise(AccountMigrationNotComplete) - {:valid_password, false} -> raise(InvalidCredentials) + {:error, :user_not_found} -> ErrorHelpers.render_json_error(conn, 400, "Invalid credentials") + {:not_confirmed, true} -> ErrorHelpers.render_json_error(conn, 400, "User account not confirmed") + {:missing_passhash, false} -> ErrorHelpers.render_json_error(conn, 403, "User account migration not complete, please reset password") + {:valid_password, false} -> ErrorHelpers.render_json_error(conn, 400, "Invalid credentials") {:error, :unban_error} -> ErrorHelpers.render_json_error(conn, 500, "There was an issue unbanning user, upon login") _ -> ErrorHelpers.render_json_error(conn, 500, "There was an issue while attempting to login") end end def login(_conn, _attrs), do: raise(InvalidPayload) + end diff --git a/lib/epochtalk_server_web/errors/custom_errors.ex b/lib/epochtalk_server_web/errors/custom_errors.ex index 8806ccd8..eeb69497 100644 --- a/lib/epochtalk_server_web/errors/custom_errors.ex +++ b/lib/epochtalk_server_web/errors/custom_errors.ex @@ -1,34 +1,4 @@ defmodule EpochtalkServerWeb.CustomErrors do - - # Auth errors - defmodule InvalidCredentials do - @moduledoc """ - Exception raised when user and password are not correct - """ - defexception plug_status: 400, message: "Invalid credentials", conn: nil, router: nil - end - - defmodule NotLoggedIn do - @moduledoc """ - Exception raised when user is not logged in - """ - defexception plug_status: 400, message: "Not logged in", conn: nil, router: nil - end - - defmodule AccountNotConfirmed do - @moduledoc """ - Exception raised when user's account is not confirmed - """ - defexception plug_status: 400, message: "User account not confirmed", conn: nil, router: nil - end - - defmodule AccountMigrationNotComplete do - @moduledoc """ - Exception raised when user's account is not fully migrated - """ - defexception plug_status: 403, message: "User account migration not complete, please reset password", conn: nil, router: nil - end - # API Payload Handling defmodule InvalidPayload do @moduledoc """ diff --git a/lib/epochtalk_server_web/plugs/prepare_parse.ex b/lib/epochtalk_server_web/plugs/prepare_parse.ex index 8f62a450..bf30cab9 100644 --- a/lib/epochtalk_server_web/plugs/prepare_parse.ex +++ b/lib/epochtalk_server_web/plugs/prepare_parse.ex @@ -6,7 +6,7 @@ defmodule EpochtalkServerWeb.Plugs.PrepareParse do MalformedPayload, OversizedPayload } - @env Application.compile_env(:application_api, :env) + @env Mix.env @methods ~w(POST PUT PATCH) @doc """ @@ -19,7 +19,7 @@ defmodule EpochtalkServerWeb.Plugs.PrepareParse do """ def call(conn, opts) do %{method: method} = conn - if method in @methods and not (@env in [:test]) do + if method in @methods and @env != :test do case Plug.Conn.read_body(conn, opts) do {:error, :timeout} -> raise Plug.TimeoutError {:error, _} -> raise Plug.BadRequestError diff --git a/mix.exs b/mix.exs index fda928fd..1d202989 100644 --- a/mix.exs +++ b/mix.exs @@ -35,6 +35,7 @@ defmodule EpochtalkServer.MixProject do [ {:argon2_elixir, "~> 3.0.0"}, {:corsica, "~> 1.2.0"}, + {:dialyxir, "~> 1.0", only: [:dev], runtime: false}, {:ecto_sql, "~> 3.6"}, {:ex_doc, "~> 0.28.5"}, {:gen_smtp, "~> 1.2"}, @@ -76,7 +77,7 @@ defmodule EpochtalkServer.MixProject do "seed.roles": ["run priv/repo/seed_roles.exs"], "seed.rp": ["run priv/repo/seed_roles_permissions.exs"], "seed.user": ["run priv/repo/seed_user.exs"], - test: ["ecto.create --quiet", "ecto.migrate", "test"] + test: ["ecto.create --quiet", "ecto.migrate", "test"], ] end end diff --git a/mix.lock b/mix.lock index d9ceb466..28dc136e 100644 --- a/mix.lock +++ b/mix.lock @@ -10,10 +10,12 @@ "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, "db_connection": {:hex, :db_connection, "2.4.2", "f92e79aff2375299a16bcb069a14ee8615c3414863a6fef93156aee8e86c2ff3", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4fe53ca91b99f55ea249693a0229356a08f4d1a7931d8ffa79289b145fe83668"}, "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, + "dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"}, "earmark_parser": {:hex, :earmark_parser, "1.4.28", "0bf6546eb7cd6185ae086cbc5d20cd6dbb4b428aad14c02c49f7b554484b4586", [:mix], [], "hexpm", "501cef12286a3231dc80c81352a9453decf9586977f917a96e619293132743fb"}, "ecto": {:hex, :ecto, "3.9.1", "67173b1687afeb68ce805ee7420b4261649d5e2deed8fe5550df23bab0bc4396", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c80bb3d736648df790f7f92f81b36c922d9dd3203ca65be4ff01d067f54eb304"}, "ecto_sql": {:hex, :ecto_sql, "3.9.0", "2bb21210a2a13317e098a420a8c1cc58b0c3421ab8e3acfa96417dab7817918c", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a8f3f720073b8b1ac4c978be25fa7960ed7fd44997420c304a4a2e200b596453"}, "elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"}, + "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_doc": {:hex, :ex_doc, "0.28.6", "2bbd7a143d3014fc26de9056793e97600ae8978af2ced82c2575f130b7c0d7d7", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bca1441614654710ba37a0e173079273d619f9160cbcc8cd04e6bd59f1ad0e29"}, "gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"}, "guardian": {:hex, :guardian, "2.3.0", "1e2a90e809fbd99439f5279db03fb30b7b2b2fc0d3870a0d76a84b099f1a2892", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "ced3ace74fc2b22b2b25dbe99e6d205443dd0d57d1cc25155a223b5e9656205e"}, From 0a1ee292554b691c75730fec23b1e12d40b29f63 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 17 Oct 2022 11:26:10 -1000 Subject: [PATCH 215/231] feat(configuration): add model for configuration --- lib/epochtalk_server/models/configuration.ex | 70 ++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 lib/epochtalk_server/models/configuration.ex diff --git a/lib/epochtalk_server/models/configuration.ex b/lib/epochtalk_server/models/configuration.ex new file mode 100644 index 00000000..49995474 --- /dev/null +++ b/lib/epochtalk_server/models/configuration.ex @@ -0,0 +1,70 @@ +defmodule EpochtalkServer.Models.Configuration do + use Ecto.Schema + import Ecto.Changeset + alias EpochtalkServer.Repo + alias EpochtalkServer.Models.Configuration + @moduledoc """ + `Configuration` model, for performing actions relating to frontend `Configuration` + """ + + @type t :: %__MODULE__{ + id: non_neg_integer | nil, + name: String.t() | nil, + config: map() | nil + } + schema "configurations" do + field :name, :string + field :config, :map + end + + ## === Changesets Functions === + + @doc """ + Create changeset for creating a new `Configuration` model + """ + @spec create_changeset( + configuration :: t(), + attrs :: map() | nil + ) :: %Ecto.Changeset{} + def create_changeset(configuration, attrs \\ %{}) do + configuration + |> cast(attrs, [:name, :config]) + |> validate_required([:name, :config]) + |> unique_constraint(:name) + end + + ## === Database Functions === + + @doc """ + Creates a new `Configuration` into the database + """ + @spec create( + configuration :: t() + ) :: {:ok, configuration :: t()} | {:error, Ecto.Changeset.t()} + def create(%Configuration{} = configuration), do: configuration |> create_changeset() |> Repo.insert() + + @doc """ + Gets a `Configuration` from the database by `name` + """ + @spec by_name(name :: String.t()) :: t() | nil + def by_name(name) when is_binary(name), do: Repo.get_by(Configuration, name: name) + + @doc """ + Gets default frontend `Configuration` from the database + """ + @spec get_default() :: t() | nil + def get_default(), do: by_name("default") + + @doc """ + Inserts a default `Configuration` into the database given `config_map` + """ + @spec set_default( + config_map :: map() + ) :: {:ok, configuration :: t()} | {:error, Ecto.Changeset.t()} + def set_default(config_map) when is_map(config_map) do + %Configuration{} + |> create_changeset(%{ name: "default", config: config_map }) + |> Repo.insert() + end + +end From 6a66edc7b51227d515d9fb221b4b526a1dd723d5 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 17 Oct 2022 13:52:39 -1000 Subject: [PATCH 216/231] refactor(.iex.exs): modify aliases of models --- .iex.exs | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/.iex.exs b/.iex.exs index d8518038..d1ca439b 100644 --- a/.iex.exs +++ b/.iex.exs @@ -3,20 +3,23 @@ IEx.configure(inspect: [limit: :infinity]) alias EpochtalkServer.Repo -alias EpochtalkServer.Models.Ban -alias EpochtalkServer.Models.BannedAddress -alias EpochtalkServer.Models.Board -alias EpochtalkServer.Models.BoardMapping -alias EpochtalkServer.Models.BoardModerator -alias EpochtalkServer.Models.Category -alias EpochtalkServer.Models.Invitation -alias EpochtalkServer.Models.MetadataBoard -alias EpochtalkServer.Models.Permission -alias EpochtalkServer.Models.Preference -alias EpochtalkServer.Models.Profile -alias EpochtalkServer.Models.Role -alias EpochtalkServer.Models.RolePermission -alias EpochtalkServer.Models.RoleUser -alias EpochtalkServer.Models.User +alias EpochtalkServer.Models.{ + Ban, + BannedAddress, + Board, + BoardMapping, + BoardModerator, + Category, + Configuration, + Invitation, + MetadataBoard, + Permission, + Preference, + Profile, + Role, + RolePermission, + RoleUser, + User +} reload = fn() -> r Enum.map(__ENV__.aliases, fn {_, module} -> module end) end From 705dae4ee72578314d7184cc2aa33c580c78a58a Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 17 Oct 2022 13:53:17 -1000 Subject: [PATCH 217/231] feat(frontend-config): add frontend_config variable to :epochtalk_server in config.exs --- config/config.exs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index da10a0b4..5909f251 100644 --- a/config/config.exs +++ b/config/config.exs @@ -8,7 +8,27 @@ import Config config :epochtalk_server, - ecto_repos: [EpochtalkServer.Repo] + ecto_repos: [EpochtalkServer.Repo], + frontend_config: %{ + login_required: false, + invite_only: false, + verify_registration: false, + ga_key: "UA-XXXXX-Y", + revision: nil, + website: %{ + title: "Epochtalk Forums", + description: "Open source forum software", + keywords: "open source, free forum, forum software, forum", + logo: nil, + favicon: nil, + default_avatar: "/images/avatar.png", + default_avatar_shape: "circle" + }, + portal: %{enabled: false, board_id: nil}, + emailer: %{ses_mode: false, options: %{}}, + images: %{s3_mode: false, options: %{}}, + rate_limiting: %{} + } # Configure Guardian config :epochtalk_server, EpochtalkServer.Auth.Guardian, From c76ffeda7801198f76f83dc9a04e5e143c48162d Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 17 Oct 2022 13:54:12 -1000 Subject: [PATCH 218/231] feat(preload-frontend-configs): preload frontend config on startup if available, otherwise insert front_config from config.exs --- lib/epochtalk_server/application.ex | 3 +++ lib/epochtalk_server/models/configuration.ex | 27 +++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/epochtalk_server/application.ex b/lib/epochtalk_server/application.ex index ee90dee8..9286ddbd 100644 --- a/lib/epochtalk_server/application.ex +++ b/lib/epochtalk_server/application.ex @@ -14,6 +14,9 @@ defmodule EpochtalkServer.Application do {Redix, host: redix_config()[:host], name: redix_config()[:name]}, # Start the Ecto repository EpochtalkServer.Repo, + # Warm frontend_config variable (referenced by api controllers) + # This worker starts, does its thing and dies + {Task, &EpochtalkServer.Models.Configuration.warm_frontend_config/0}, # Start the Telemetry supervisor EpochtalkServerWeb.Telemetry, # Start the PubSub system diff --git a/lib/epochtalk_server/models/configuration.ex b/lib/epochtalk_server/models/configuration.ex index 49995474..97f941ca 100644 --- a/lib/epochtalk_server/models/configuration.ex +++ b/lib/epochtalk_server/models/configuration.ex @@ -1,5 +1,6 @@ defmodule EpochtalkServer.Models.Configuration do use Ecto.Schema + import Logger, only: [debug: 1] import Ecto.Changeset alias EpochtalkServer.Repo alias EpochtalkServer.Models.Configuration @@ -64,7 +65,31 @@ defmodule EpochtalkServer.Models.Configuration do def set_default(config_map) when is_map(config_map) do %Configuration{} |> create_changeset(%{ name: "default", config: config_map }) - |> Repo.insert() + |> Repo.insert(returning: [:config]) + end + + ## === External Helper Functions === + @doc """ + Warms `:epochtalk_server[:frontend_config]` config variable using `Configuration` stored in database, + if present. If there is no `Configuration` in the database, the default value is taken + from `:epochtalk_server[:frontend_config]` and inserted into the database as the default + `Configuration`. + """ + def warm_frontend_config() do + frontend_config = case Configuration.get_default() do + nil -> + debug("Frontend Configurations not found, setting defaults in Database") + config = Application.get_env(:epochtalk_server, :frontend_config) + case Configuration.set_default(config) do + {:ok, configuration} -> configuration.config + {:error, _} -> raise("There was an issue with :epochtalk_server[:frontend_configs], please check config/config.exs") + end + configuration -> + debug("Loading Frontend Configurations from Database") + configuration.config + end + Application.put_env(:epochtalk_server, :frontend_config, frontend_config) + debug "Frontend Configuration:\n" <> inspect(frontend_config, pretty: true, syntax_colors: IO.ANSI.syntax_colors) end end From f6e584b82339ab8a148364655d65fd0d5f5aad16 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 17 Oct 2022 14:58:08 -1000 Subject: [PATCH 219/231] feat(configs): add newbie enabled to frontend config --- config/config.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/config/config.exs b/config/config.exs index 5909f251..36ad8185 100644 --- a/config/config.exs +++ b/config/config.exs @@ -10,6 +10,7 @@ import Config config :epochtalk_server, ecto_repos: [EpochtalkServer.Repo], frontend_config: %{ + newbie_enabled: true, login_required: false, invite_only: false, verify_registration: false, From ddab4930a1bc84125c535c2f55020c8e64b24365 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Mon, 17 Oct 2022 14:58:36 -1000 Subject: [PATCH 220/231] feat(config-revision): add git revision to config --- lib/epochtalk_server/models/configuration.ex | 30 ++++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/lib/epochtalk_server/models/configuration.ex b/lib/epochtalk_server/models/configuration.ex index 97f941ca..1db9819a 100644 --- a/lib/epochtalk_server/models/configuration.ex +++ b/lib/epochtalk_server/models/configuration.ex @@ -69,6 +69,7 @@ defmodule EpochtalkServer.Models.Configuration do end ## === External Helper Functions === + @doc """ Warms `:epochtalk_server[:frontend_config]` config variable using `Configuration` stored in database, if present. If there is no `Configuration` in the database, the default value is taken @@ -77,19 +78,38 @@ defmodule EpochtalkServer.Models.Configuration do """ def warm_frontend_config() do frontend_config = case Configuration.get_default() do - nil -> + nil -> # no configurations in database debug("Frontend Configurations not found, setting defaults in Database") - config = Application.get_env(:epochtalk_server, :frontend_config) - case Configuration.set_default(config) do + frontend_config = Application.get_env(:epochtalk_server, :frontend_config) + case Configuration.set_default(frontend_config) do {:ok, configuration} -> configuration.config {:error, _} -> raise("There was an issue with :epochtalk_server[:frontend_configs], please check config/config.exs") end - configuration -> + configuration -> # configuration found in database debug("Loading Frontend Configurations from Database") configuration.config end Application.put_env(:epochtalk_server, :frontend_config, frontend_config) - debug "Frontend Configuration:\n" <> inspect(frontend_config, pretty: true, syntax_colors: IO.ANSI.syntax_colors) + set_git_revision() + debug "Frontend Configuration:\n" + <> inspect( + Application.get_env(:epochtalk_server, :frontend_config), + pretty: true, + syntax_colors: IO.ANSI.syntax_colors + ) end + ## === Private Helper Functions === + + defp set_git_revision() do + # revision + {rev, _} = System.cmd("git", ["rev-parse", "--short", "HEAD"]) + [rev | _] = rev |> String.split("\n") + # TODO(boka): git tag --points-at HEAD v[0-9]* + # TODO(boka): directory release version + + frontend_config = Application.get_env(:epochtalk_server, :frontend_config) + |> Map.put("revision", rev) + Application.put_env(:epochtalk_server, :frontend_config, frontend_config) + end end From 088b5f2e056da78ca8d51076fca25972f1dc28a3 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 18 Oct 2022 12:02:43 -1000 Subject: [PATCH 221/231] feat(config): add missing frontend configs --- config/config.exs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/config/config.exs b/config/config.exs index 36ad8185..fa077757 100644 --- a/config/config.exs +++ b/config/config.exs @@ -10,10 +10,11 @@ import Config config :epochtalk_server, ecto_repos: [EpochtalkServer.Repo], frontend_config: %{ - newbie_enabled: true, + public_url: "http://localhost:8000", + newbie_enabled: false, login_required: false, invite_only: false, - verify_registration: false, + verify_registration: true, ga_key: "UA-XXXXX-Y", revision: nil, website: %{ @@ -26,7 +27,7 @@ config :epochtalk_server, default_avatar_shape: "circle" }, portal: %{enabled: false, board_id: nil}, - emailer: %{ses_mode: false, options: %{}}, + emailer: %{ses_mode: false, options: %{from_address: "info@epochtalk.com"}}, images: %{s3_mode: false, options: %{}}, rate_limiting: %{} } From 632f8a965c270e0ba126cf8e432488d2c23b7a8c Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 18 Oct 2022 12:21:27 -1000 Subject: [PATCH 222/231] refactor(dialyzer): refactor to get dialyzer to pass with frontend config warmer --- lib/epochtalk_server/application.ex | 11 ++++++++++- lib/epochtalk_server/models/configuration.ex | 3 ++- lib/epochtalk_server_web/router.ex | 9 +++++++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/lib/epochtalk_server/application.ex b/lib/epochtalk_server/application.ex index 9286ddbd..66ac821f 100644 --- a/lib/epochtalk_server/application.ex +++ b/lib/epochtalk_server/application.ex @@ -4,9 +4,13 @@ defmodule EpochtalkServer.Application do @moduledoc false use Application + @env Mix.env() @impl true def start(_type, _args) do + # Set Environment to config + # Work around for dialyzer rendering @env as :dev + Application.put_env(:epochtalk_server, :env, @env) children = [ # Start Guardian Redis Redix connection GuardianRedis.Redix, @@ -27,6 +31,11 @@ defmodule EpochtalkServer.Application do # {EpochtalkServer.Worker, arg} ] + # don't run config_warmer during tests + children = if Application.get_env(:epochtalk_server, :env) == :test, + do: List.delete(children, {Task, &EpochtalkServer.Models.Configuration.warm_frontend_config/0}), + else: children + # See https://hexdocs.pm/elixir/Supervisor.html # for other strategies and supported options opts = [strategy: :one_for_one, name: EpochtalkServer.Supervisor] @@ -42,5 +51,5 @@ defmodule EpochtalkServer.Application do end # fetch redix config - defp redix_config, do: Application.get_env(:epochtalk_server, :redix) + defp redix_config(), do: Application.get_env(:epochtalk_server, :redix) end diff --git a/lib/epochtalk_server/models/configuration.ex b/lib/epochtalk_server/models/configuration.ex index 1db9819a..149290be 100644 --- a/lib/epochtalk_server/models/configuration.ex +++ b/lib/epochtalk_server/models/configuration.ex @@ -74,8 +74,9 @@ defmodule EpochtalkServer.Models.Configuration do Warms `:epochtalk_server[:frontend_config]` config variable using `Configuration` stored in database, if present. If there is no `Configuration` in the database, the default value is taken from `:epochtalk_server[:frontend_config]` and inserted into the database as the default - `Configuration`. + `Configuration`. Run as a Task on application startup. """ + @spec warm_frontend_config() :: :ok def warm_frontend_config() do frontend_config = case Configuration.get_default() do nil -> # no configurations in database diff --git a/lib/epochtalk_server_web/router.ex b/lib/epochtalk_server_web/router.ex index be0bee42..3f70c198 100644 --- a/lib/epochtalk_server_web/router.ex +++ b/lib/epochtalk_server_web/router.ex @@ -1,6 +1,6 @@ defmodule EpochtalkServerWeb.Router do use EpochtalkServerWeb, :router - + @env Mix.env pipeline :api do plug :accepts, ["json"] end @@ -27,14 +27,19 @@ defmodule EpochtalkServerWeb.Router do get "/register/email/:email", UserController, :email post "/register", UserController, :register post "/login", UserController, :login + post "/confirm", UserController, :confirm delete "/logout", UserController, :logout end + scope "/", EpochtalkServerWeb do + get "/config.js", ConfigurationController, :config_file + end + # Enables the Swoosh mailbox preview in development. # # Note that preview only shows emails that were sent by the same # node running the Phoenix server. - if Mix.env == :dev do + if @env in [:dev] do scope "/dev" do forward "/mailbox", Plug.Swoosh.MailboxPreview end From ec14926df61e31a822f5eef91e199207c95b4d84 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 18 Oct 2022 12:22:26 -1000 Subject: [PATCH 223/231] feat(git-revision): modify code that sets git revision to fetch tag --- lib/epochtalk_server/models/configuration.ex | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/epochtalk_server/models/configuration.ex b/lib/epochtalk_server/models/configuration.ex index 149290be..cdc6ce80 100644 --- a/lib/epochtalk_server/models/configuration.ex +++ b/lib/epochtalk_server/models/configuration.ex @@ -98,19 +98,28 @@ defmodule EpochtalkServer.Models.Configuration do pretty: true, syntax_colors: IO.ANSI.syntax_colors ) + :ok end ## === Private Helper Functions === defp set_git_revision() do + # tag + {tag, _} = System.cmd("git", ["tag", "--points-at", "HEAD", "v[0-9]*"]) + IO.inspect tag + [tag | _] = tag |> String.split("\n") + # revision - {rev, _} = System.cmd("git", ["rev-parse", "--short", "HEAD"]) - [rev | _] = rev |> String.split("\n") - # TODO(boka): git tag --points-at HEAD v[0-9]* + {hash, _} = System.cmd("git", ["rev-parse", "--short", "HEAD"]) + [hash | _] = hash |> String.split("\n") + # TODO(boka): directory release version + # tag takes precidence over revision + revision = unless tag == "", do: tag, else: hash + frontend_config = Application.get_env(:epochtalk_server, :frontend_config) - |> Map.put("revision", rev) + |> Map.put("revision", revision) Application.put_env(:epochtalk_server, :frontend_config, frontend_config) end end From 6368e9a485757862b606a914d2348566855963c1 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 18 Oct 2022 13:41:59 -1000 Subject: [PATCH 224/231] feat(newbie-config): handle newbie config, assign newbie role instead of user by default if set --- lib/epochtalk_server/models/role.ex | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/epochtalk_server/models/role.ex b/lib/epochtalk_server/models/role.ex index f0cf8c25..d7adc2d0 100644 --- a/lib/epochtalk_server/models/role.ex +++ b/lib/epochtalk_server/models/role.ex @@ -70,10 +70,15 @@ defmodule EpochtalkServer.Models.Role do def get_newbie_role_id(), do: Repo.one(from(r in Role, select: r.id, where: r.lookup == "newbie")) @doc """ - Returns default `Role`, for base installation this is the `user` role + Returns default `Role`, for base installation this is the `user` role, if `:epochtalk_server[:frontend_config]["newbie_enabled"]` + configuration is set to true, then `newbie` is the default role. """ @spec get_default() :: t() | nil - def get_default(), do: by_lookup("user") + def get_default() do + config = Application.get_env(:epochtalk_server, :frontend_config) + newbie_enabled = config["newbie_enabled"] + by_lookup(if newbie_enabled, do: "newbie", else: "user") + end @doc """ Returns a `Role` or list of roles, for specified lookup(s) From 1a514744381d02a7c32334a487a3c145abfab55b Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 18 Oct 2022 13:45:19 -1000 Subject: [PATCH 225/231] feat(auth-configs): update existing auth routes to use frontend configs --- lib/epochtalk_server/mailer.ex | 18 ++++++++++-- lib/epochtalk_server/models/user.ex | 2 +- .../controllers/user_controller.ex | 29 ++++++++++++++++--- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/lib/epochtalk_server/mailer.ex b/lib/epochtalk_server/mailer.ex index aecd61fc..147b7a5c 100644 --- a/lib/epochtalk_server/mailer.ex +++ b/lib/epochtalk_server/mailer.ex @@ -1,9 +1,21 @@ defmodule EpochtalkServer.Mailer do use Swoosh.Mailer, otp_app: :epochtalk_server + require Logger import Swoosh.Email - def test_email() do - email = new(from: {"Dr B Banner", "hulk.smash@example.com"}, to: {"Tony Stark", "iron.man@mailinator.com"}, subject: "Hello, Avengers!", text_body: "HELLO WORLD") - deliver(email) + @spec send_confirm_account(recipient :: User.t()) :: {:ok, term} | {:error, term} + def send_confirm_account(%User{ email: email, username: username, confirmation_token: token}) do + new() + |> to({username, email}) + |> from({"Epochtalk", "info@epochtalk.com"}) + |> subject("[Epochtalk] Account Confirmation") + |> html_body("TODO") + |> deliver() + |> case do + {:ok, email_metadata} -> {:ok, email_metadata} + {:error, error} -> + Logger.error(error) # Log error + {:error, :not_delivered} # return error we can match + end end end diff --git a/lib/epochtalk_server/models/user.ex b/lib/epochtalk_server/models/user.ex index dca4a744..fcff3f75 100644 --- a/lib/epochtalk_server/models/user.ex +++ b/lib/epochtalk_server/models/user.ex @@ -68,7 +68,7 @@ defmodule EpochtalkServer.Models.User do @spec registration_changeset(user :: t(), attrs :: map() | nil) :: %Ecto.Changeset{} def registration_changeset(user, attrs) do user - |> cast(attrs, [:id, :email, :username, :created_at, :updated_at, :deleted, :malicious_score, :password]) + |> cast(attrs, [:id, :email, :confirmation_token, :username, :created_at, :updated_at, :deleted, :malicious_score, :password]) |> unique_constraint(:id, name: :users_pkey) |> validate_username() |> validate_email() diff --git a/lib/epochtalk_server_web/controllers/user_controller.ex b/lib/epochtalk_server_web/controllers/user_controller.ex index cba34da2..d0b431b7 100644 --- a/lib/epochtalk_server_web/controllers/user_controller.ex +++ b/lib/epochtalk_server_web/controllers/user_controller.ex @@ -8,6 +8,7 @@ defmodule EpochtalkServerWeb.UserController do alias EpochtalkServer.Models.Invitation alias EpochtalkServer.Auth.Guardian alias EpochtalkServer.Session + alias EpochtalkServer.Mailer alias EpochtalkServerWeb.ErrorHelpers alias EpochtalkServerWeb.CustomErrors.InvalidPayload @@ -23,13 +24,12 @@ defmodule EpochtalkServerWeb.UserController do @doc """ Registers a new `User` - - - TODO(akinsey): handle config.inviteOnly - - TODO(akinsey): handle config.newbieEnabled - - TODO(akinsey): Send confirmation email """ def register(conn, %{"username" => _, "email" => _, "password" => _} = attrs) do + config = Application.get_env(:epochtalk_server, :frontend_config) with {:auth, false} <- {:auth, Guardian.Plug.authenticated?(conn)}, # check auth + {:invite_only, false} <- {:invite_only, !!config["invite_only"]}, + {:verify_registration, false} <- {:verify_registration, !!config["verify_registration"]}, {:ok, user} <- User.create(attrs), # create user {_count, nil} <- Invitation.delete(user.email), # delete invitation {:ok, user} <- User.handle_malicious_user(user, conn.remote_ip), # ban if malicious @@ -38,6 +38,10 @@ defmodule EpochtalkServerWeb.UserController do else # user already authenticated {:auth, true} -> ErrorHelpers.render_json_error(conn, 400, "Cannot register a new account while logged in") + # forum is set to invite only + {:invite_only, true} -> ErrorHelpers.render_json_error(conn, 423, "Registration is closed, forum is invite only") + # verify registration is on, run register_but_verify instead + {:verify_registration, true} -> register_but_verify(conn, attrs) # error banning in handle_malicious_user {:error, :ban_error} -> ErrorHelpers.render_json_error(conn, 500, "There was an error banning malicious user, upon login") # error in user.create @@ -48,6 +52,23 @@ defmodule EpochtalkServerWeb.UserController do end def register(_conn, _attrs), do: raise(InvalidPayload) + defp register_but_verify(conn, attrs) do + # add confirmation token + attrs = attrs |> Map.put("confirmation_token", :crypto.strong_rand_bytes(20) |> Base.encode64) + with {:ok, user} <- User.create(attrs), # create user + {_count, nil} <- Invitation.delete(user.email), # delete invitation + {:ok, _email} <- Mailer.send_confirm_account(user) do # send confirm account email + render(conn, "register_but_verify.json", user: user) + else + # error in user.create + {:error, data} -> ErrorHelpers.render_json_error(conn, 400, data) + # error email failed to send + {:error, :not_delivered} -> ErrorHelpers.render_json_error(conn, 500, "Sending of account confirmation email failed, mailer is not properly configured.") + # Catch all for any other errors + _ -> ErrorHelpers.render_json_error(conn, 500, "There was an issue registering") + end + end + @doc """ Authenticates currently logged in `User` """ From 2c536b369985b5e039754f0822bd27396416b233 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Tue, 18 Oct 2022 15:10:11 -1000 Subject: [PATCH 226/231] feat(email-templates): rough draft of email templates --- lib/epochtalk_server/mailer.ex | 95 ++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/lib/epochtalk_server/mailer.ex b/lib/epochtalk_server/mailer.ex index 147b7a5c..9b3c34fb 100644 --- a/lib/epochtalk_server/mailer.ex +++ b/lib/epochtalk_server/mailer.ex @@ -2,14 +2,26 @@ defmodule EpochtalkServer.Mailer do use Swoosh.Mailer, otp_app: :epochtalk_server require Logger import Swoosh.Email + alias EpochtalkServer.Models.User @spec send_confirm_account(recipient :: User.t()) :: {:ok, term} | {:error, term} def send_confirm_account(%User{ email: email, username: username, confirmation_token: token}) do + config = Application.get_env(:epochtalk_server, :frontend_config) + public_url = config["public_url"] + website_title = config["website"]["title"] + from_address = config["emailer"]["options"]["from_address"] + confirm_url = "#{public_url}/confirm/#{String.downcase(username)}/#{token}" + content = generate_from_base_template(""" +

Account Confirmation

+ Please visit the link below to complete the registration process for the account #{username}.

+ Confirm Account

+ Raw confirm URL: #{confirm_url} + """, config) new() |> to({username, email}) - |> from({"Epochtalk", "info@epochtalk.com"}) - |> subject("[Epochtalk] Account Confirmation") - |> html_body("TODO") + |> from({website_title, from_address}) + |> subject("[#{website_title}] Account Confirmation") + |> html_body(content) |> deliver() |> case do {:ok, email_metadata} -> {:ok, email_metadata} @@ -18,4 +30,81 @@ defmodule EpochtalkServer.Mailer do {:error, :not_delivered} # return error we can match end end + + defp generate_from_base_template(content, config) do + # TODO(akinsey): Fetch values from css files + css = %{ + header_bg_color: "rgb(66, 71, 80)", + header_font_color: "rgb(255, 255, 255)", + base_background_color: "rgb(255, 255, 255)", + border_color: "rgb(215, 215, 215)", + base_font_color: "rgb(34, 34, 54)", + sub_header_color: "gb(245, 245, 245)", + secondary_font_color: "rgb(153, 153, 153)", + color_primary: "rgb(255, 100, 0)" + } + website_title = config["website"]["title"] + """ + + + + + + + + + + + + + + + + + + +
+ #{website_title} +
+ #{content} +
+ +
+ + + """ + end end From 15445fd7856220b893bedf3512483b011c003040 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 20 Oct 2022 11:45:13 -1000 Subject: [PATCH 227/231] feat(confirm-route): add confirm route now that we can handle invite only --- .../controllers/user_controller.ex | 20 +++++++++++++++++++ lib/epochtalk_server_web/views/user_view.ex | 18 +++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/lib/epochtalk_server_web/controllers/user_controller.ex b/lib/epochtalk_server_web/controllers/user_controller.ex index d0b431b7..69072b83 100644 --- a/lib/epochtalk_server_web/controllers/user_controller.ex +++ b/lib/epochtalk_server_web/controllers/user_controller.ex @@ -69,6 +69,26 @@ defmodule EpochtalkServerWeb.UserController do end end + @doc """ + Confirms a newly registered `User`. Used when `:epochtalk_server[:frontend_config]["verify_registration"]` + configuration is set to `true` + """ + def confirm(conn, %{"username" => username, "token" => token}) do + with {:ok, user} <- User.by_username(username), # create user + {:token_valid, true} <- {:token_valid, user.confirmation_token == token}, # check confirmation token + {:ok, user} <- User.handle_malicious_user(user, conn.remote_ip), # ban if malicious + {:ok, user, token, conn} <- Session.create(user, false, conn) do # create session + render(conn, "user.json", %{ user: user, token: token}) + else + {:error, :user_not_found} -> ErrorHelpers.render_json_error(conn, 400, "Confirmation error, account not found") + {:token_valid, false} -> ErrorHelpers.render_json_error(conn, 400, "Account confirmation error, invalid token") + {:error, :ban_error} -> ErrorHelpers.render_json_error(conn, 500, "There was an error banning malicious user, upon confirming account") + {:error, data} -> ErrorHelpers.render_json_error(conn, 400, data) + _ -> ErrorHelpers.render_json_error(conn, 500, "There was an issue registering") + end + end + def confirm(_conn, _attrs), do: raise(InvalidPayload) + @doc """ Authenticates currently logged in `User` """ diff --git a/lib/epochtalk_server_web/views/user_view.ex b/lib/epochtalk_server_web/views/user_view.ex index c196f73b..1c6220e5 100644 --- a/lib/epochtalk_server_web/views/user_view.ex +++ b/lib/epochtalk_server_web/views/user_view.ex @@ -13,6 +13,24 @@ defmodule EpochtalkServerWeb.UserView do """ def render("user.json", %{user: user, token: token}), do: format_user_reply(user, token) + @doc """ + Renders formatted JSON response for registration confirmation. + ## Example + iex> EpochtalkServerWeb.UserView.render("register_with_verify.json", %{user: %User{ username: "Test" }}) + %{ + username: "Test", + confirm_token: true, + message: "Successfully registered, please confirm account to login." + } + """ + def render("register_with_verify.json", %{user: user}) do + %{ + username: user.username, + confirm_token: true, + message: "Successfully registered, please confirm account to login." + } + end + @doc """ Renders whatever data it is passed when template not found. Data pass through for rendering misc responses (ex: {found: true} or {success: true}) From aab75715d0a8638ce7b4adbd2a4621d8864283ae Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 20 Oct 2022 13:21:49 -1000 Subject: [PATCH 228/231] refactor(frontend-configs): add missing frontend configs --- config/config.exs | 9 +++++++-- lib/epochtalk_server/mailer.ex | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/config/config.exs b/config/config.exs index fa077757..ded450ce 100644 --- a/config/config.exs +++ b/config/config.exs @@ -10,11 +10,16 @@ import Config config :epochtalk_server, ecto_repos: [EpochtalkServer.Repo], frontend_config: %{ - public_url: "http://localhost:8000", + frontend_url: "http://localhost:8000", + backend_url: "http://localhost:4000", newbie_enabled: false, login_required: false, invite_only: false, verify_registration: true, + post_max_length: 10_000, + max_image_size: 10_485_760, + max_avatar_size: 102_400, + mobile_break_width: 767, ga_key: "UA-XXXXX-Y", revision: nil, website: %{ @@ -28,7 +33,7 @@ config :epochtalk_server, }, portal: %{enabled: false, board_id: nil}, emailer: %{ses_mode: false, options: %{from_address: "info@epochtalk.com"}}, - images: %{s3_mode: false, options: %{}}, + images: %{s3_mode: false, options: %{ local_host: "http://localhost:4000" }}, rate_limiting: %{} } diff --git a/lib/epochtalk_server/mailer.ex b/lib/epochtalk_server/mailer.ex index 9b3c34fb..d7ffe3b8 100644 --- a/lib/epochtalk_server/mailer.ex +++ b/lib/epochtalk_server/mailer.ex @@ -7,10 +7,10 @@ defmodule EpochtalkServer.Mailer do @spec send_confirm_account(recipient :: User.t()) :: {:ok, term} | {:error, term} def send_confirm_account(%User{ email: email, username: username, confirmation_token: token}) do config = Application.get_env(:epochtalk_server, :frontend_config) - public_url = config["public_url"] + frontend_url = config["frontend_url"] website_title = config["website"]["title"] from_address = config["emailer"]["options"]["from_address"] - confirm_url = "#{public_url}/confirm/#{String.downcase(username)}/#{token}" + confirm_url = "#{frontend_url}/confirm/#{String.downcase(username)}/#{token}" content = generate_from_base_template("""

Account Confirmation

Please visit the link below to complete the registration process for the account #{username}.

From e5845f4d8ce9e6a29343ad9511ca52af6331f1c5 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Thu, 20 Oct 2022 15:38:32 -1000 Subject: [PATCH 229/231] feat(frontend-config): serve /config.js using data in :epochtalk_server[:frontend_config] --- .../controllers/configuration_controller.ex | 15 +++++++++++++ lib/epochtalk_server_web/router.ex | 2 +- .../templates/configuration/config.js.eex | 22 +++++++++++++++++++ .../views/configuration_view.ex | 6 +++++ mix.exs | 1 + mix.lock | 1 + 6 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 lib/epochtalk_server_web/controllers/configuration_controller.ex create mode 100644 lib/epochtalk_server_web/templates/configuration/config.js.eex create mode 100644 lib/epochtalk_server_web/views/configuration_view.ex diff --git a/lib/epochtalk_server_web/controllers/configuration_controller.ex b/lib/epochtalk_server_web/controllers/configuration_controller.ex new file mode 100644 index 00000000..64a6cc31 --- /dev/null +++ b/lib/epochtalk_server_web/controllers/configuration_controller.ex @@ -0,0 +1,15 @@ +defmodule EpochtalkServerWeb.ConfigurationController do + use EpochtalkServerWeb, :controller + @moduledoc """ + Controller For `Configuration` related API requests + """ + + @doc """ + Used to render `/config.js` which is used by the Epochtalk Vue Frontend + """ + def config(conn, _attrs) do + conn + |> put_resp_header("content-type", "text/javascript") + |> render("config.js", config: Application.get_env(:epochtalk_server, :frontend_config)) + end +end diff --git a/lib/epochtalk_server_web/router.ex b/lib/epochtalk_server_web/router.ex index 3f70c198..9380f979 100644 --- a/lib/epochtalk_server_web/router.ex +++ b/lib/epochtalk_server_web/router.ex @@ -32,7 +32,7 @@ defmodule EpochtalkServerWeb.Router do end scope "/", EpochtalkServerWeb do - get "/config.js", ConfigurationController, :config_file + get "/config.js", ConfigurationController, :config end # Enables the Swoosh mailbox preview in development. diff --git a/lib/epochtalk_server_web/templates/configuration/config.js.eex b/lib/epochtalk_server_web/templates/configuration/config.js.eex new file mode 100644 index 00000000..d56f91dc --- /dev/null +++ b/lib/epochtalk_server_web/templates/configuration/config.js.eex @@ -0,0 +1,22 @@ +window.title = "<%= @config["website"]["title"] %>" +window.description = "<%= @config["website"]["description"] %>" +window.keywords = "<%= @config["website"]["keywords"] %>" +window.logo = "<%= @config["website"]["logo"] %>" +window.default_avatar = "<%= @config["website"]["default_avatar"] %>" +window.default_avatar_shape = "<%= @config["website"]["default_avatar_shape"] %>" +window.favicon = "<%= @config["website"]["favicon"] %>" +window.images_local_root = "<%= @config["images"]["options"]["local_host"] %>" +window.post_max_length = "<%= @config["post_max_length"] %>" +window.mobile_break_width = "<%= @config["mobile_break_width"] %>" +window.max_image_size = "<%= @config["max_image_size"] %>" +window.max_avatar_size = "<%= @config["max_avatar_size"] %>" +window.frontend_url = "<%= @config["frontend_url"] %>" +window.backend_url = "<%= @config["backend_url"] %>" +window.portal = { + enabled: <%= @config["portal"]["enabled"] %>, + board_id: <%= if board_id = @config["portal"]["board_id"], do: board_id, else: "null" %> +}; +window.ga_key = "<%= @config["ga_key"] %>" +window.google_api_key = "<%= @config["google_api_key"] %>" +window.google_client_id = "<%= @config["google_client_id"] %>" +window.google_app_domain = "<%= @config["google_app_domain"] %>" diff --git a/lib/epochtalk_server_web/views/configuration_view.ex b/lib/epochtalk_server_web/views/configuration_view.ex new file mode 100644 index 00000000..a3ef676f --- /dev/null +++ b/lib/epochtalk_server_web/views/configuration_view.ex @@ -0,0 +1,6 @@ +defmodule EpochtalkServerWeb.ConfigurationView do + use EpochtalkServerWeb, :view + @moduledoc """ + Renders and formats `Configuration` data, in JSON format for frontend + """ +end diff --git a/mix.exs b/mix.exs index 1d202989..36d46b64 100644 --- a/mix.exs +++ b/mix.exs @@ -46,6 +46,7 @@ defmodule EpochtalkServer.MixProject do {:jason, "~> 1.3.0"}, {:phoenix, "~> 1.6.11"}, {:phoenix_ecto, "~> 4.4"}, + {:phoenix_html, "~> 3.0"}, {:plug_cowboy, "~> 2.5"}, {:postgrex, ">= 0.0.0"}, {:redix, "~> 1.1.5"}, diff --git a/mix.lock b/mix.lock index 28dc136e..f6002b08 100644 --- a/mix.lock +++ b/mix.lock @@ -31,6 +31,7 @@ "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, "phoenix": {:hex, :phoenix, "1.6.14", "57678366dc1d5bad49832a0fc7f12c2830c10d3eacfad681bfe9602cd4445f04", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d48c0da00b3d4cd1aad6055387917491af9f6e1f1e96cedf6c6b7998df9dba26"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"}, + "phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"}, "phoenix_view": {:hex, :phoenix_view, "1.1.2", "1b82764a065fb41051637872c7bd07ed2fdb6f5c3bd89684d4dca6e10115c95a", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "7ae90ad27b09091266f6adbb61e1d2516a7c3d7062c6789d46a7554ec40f3a56"}, "plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"}, From aae6695574917b98878726dbd9b3c76ac1c36143 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Fri, 21 Oct 2022 12:01:02 -1000 Subject: [PATCH 230/231] fix(register_with_verify): name change register_but_verify -> register_with_verify --- lib/epochtalk_server_web/controllers/user_controller.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/epochtalk_server_web/controllers/user_controller.ex b/lib/epochtalk_server_web/controllers/user_controller.ex index 69072b83..ca678b9b 100644 --- a/lib/epochtalk_server_web/controllers/user_controller.ex +++ b/lib/epochtalk_server_web/controllers/user_controller.ex @@ -40,8 +40,8 @@ defmodule EpochtalkServerWeb.UserController do {:auth, true} -> ErrorHelpers.render_json_error(conn, 400, "Cannot register a new account while logged in") # forum is set to invite only {:invite_only, true} -> ErrorHelpers.render_json_error(conn, 423, "Registration is closed, forum is invite only") - # verify registration is on, run register_but_verify instead - {:verify_registration, true} -> register_but_verify(conn, attrs) + # verify registration is on, run register_with_verify instead + {:verify_registration, true} -> register_with_verify(conn, attrs) # error banning in handle_malicious_user {:error, :ban_error} -> ErrorHelpers.render_json_error(conn, 500, "There was an error banning malicious user, upon login") # error in user.create @@ -52,13 +52,13 @@ defmodule EpochtalkServerWeb.UserController do end def register(_conn, _attrs), do: raise(InvalidPayload) - defp register_but_verify(conn, attrs) do + defp register_with_verify(conn, attrs) do # add confirmation token attrs = attrs |> Map.put("confirmation_token", :crypto.strong_rand_bytes(20) |> Base.encode64) with {:ok, user} <- User.create(attrs), # create user {_count, nil} <- Invitation.delete(user.email), # delete invitation {:ok, _email} <- Mailer.send_confirm_account(user) do # send confirm account email - render(conn, "register_but_verify.json", user: user) + render(conn, "register_with_verify.json", user: user) else # error in user.create {:error, data} -> ErrorHelpers.render_json_error(conn, 400, data) From e31231a0ff1c6bc5455e7cc73c0311047dc15f64 Mon Sep 17 00:00:00 2001 From: Anthony Kinsey Date: Fri, 21 Oct 2022 12:01:24 -1000 Subject: [PATCH 231/231] refactor(config.js): use single quotes for js sytnax --- .../templates/configuration/config.js.eex | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/epochtalk_server_web/templates/configuration/config.js.eex b/lib/epochtalk_server_web/templates/configuration/config.js.eex index d56f91dc..dba808e4 100644 --- a/lib/epochtalk_server_web/templates/configuration/config.js.eex +++ b/lib/epochtalk_server_web/templates/configuration/config.js.eex @@ -1,22 +1,22 @@ -window.title = "<%= @config["website"]["title"] %>" -window.description = "<%= @config["website"]["description"] %>" -window.keywords = "<%= @config["website"]["keywords"] %>" -window.logo = "<%= @config["website"]["logo"] %>" -window.default_avatar = "<%= @config["website"]["default_avatar"] %>" -window.default_avatar_shape = "<%= @config["website"]["default_avatar_shape"] %>" -window.favicon = "<%= @config["website"]["favicon"] %>" -window.images_local_root = "<%= @config["images"]["options"]["local_host"] %>" -window.post_max_length = "<%= @config["post_max_length"] %>" -window.mobile_break_width = "<%= @config["mobile_break_width"] %>" -window.max_image_size = "<%= @config["max_image_size"] %>" -window.max_avatar_size = "<%= @config["max_avatar_size"] %>" -window.frontend_url = "<%= @config["frontend_url"] %>" -window.backend_url = "<%= @config["backend_url"] %>" +window.title = '<%= @config["website"]["title"] %>' +window.description = '<%= @config["website"]["description"] %>' +window.keywords = '<%= @config["website"]["keywords"] %>' +window.logo = '<%= @config["website"]["logo"] %>' +window.default_avatar = '<%= @config["website"]["default_avatar"] %>' +window.default_avatar_shape = '<%= @config["website"]["default_avatar_shape"] %>' +window.favicon = '<%= @config["website"]["favicon"] %>' +window.images_local_root = '<%= @config["images"]["options"]["local_host"] %>' +window.post_max_length = '<%= @config["post_max_length"] %>' +window.mobile_break_width = '<%= @config["mobile_break_width"] %>' +window.max_image_size = '<%= @config["max_image_size"] %>' +window.max_avatar_size = '<%= @config["max_avatar_size"] %>' +window.frontend_url = '<%= @config["frontend_url"] %>' +window.backend_url = '<%= @config["backend_url"] %>' window.portal = { enabled: <%= @config["portal"]["enabled"] %>, board_id: <%= if board_id = @config["portal"]["board_id"], do: board_id, else: "null" %> }; -window.ga_key = "<%= @config["ga_key"] %>" -window.google_api_key = "<%= @config["google_api_key"] %>" -window.google_client_id = "<%= @config["google_client_id"] %>" -window.google_app_domain = "<%= @config["google_app_domain"] %>" +window.ga_key = '<%= @config["ga_key"] %>' +window.google_api_key = '<%= @config["google_api_key"] %>' +window.google_client_id = '<%= @config["google_client_id"] %>' +window.google_app_domain = '<%= @config["google_app_domain"] %>'