From 9fa6968f6fef99b229e391c872de06b425964483 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 24 Mar 2019 11:00:46 -0400 Subject: [PATCH] Add authentication for Fever API --- lib/frenzy/user.ex | 19 +- lib/frenzy_web/controllers/feed_controller.ex | 2 +- .../controllers/fever_controller.ex | 179 ++++++++++++------ lib/mix/tasks/frenzy/user.ex | 4 +- .../20190324143311_users_add_fever_token.exs | 9 + 5 files changed, 145 insertions(+), 68 deletions(-) create mode 100644 priv/repo/migrations/20190324143311_users_add_fever_token.exs diff --git a/lib/frenzy/user.ex b/lib/frenzy/user.ex index fe6c352..5a09bc1 100644 --- a/lib/frenzy/user.ex +++ b/lib/frenzy/user.ex @@ -6,6 +6,8 @@ defmodule Frenzy.User do field :username, :string field :password, :string, virtual: true field :password_hash, :string + field :fever_password, :string, virtual: true + field :fever_auth_token, :string has_many :groups, Frenzy.Group, on_delete: :delete_all @@ -21,16 +23,23 @@ defmodule Frenzy.User do def registration_changeset(user, attrs) do user - |> cast(attrs, [:username, :password]) + |> cast(attrs, [:username, :password, :fever_password]) |> validate_length(:password, min: 8) + |> validate_length(:fever_password, min: 8) |> put_password_hash() end defp put_password_hash( - %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset + %Ecto.Changeset{ + valid?: true, + changes: %{username: username, password: password, fever_password: fever_password} + } = changeset ) do - change(changeset, Bcrypt.add_hash(password)) + changeset + |> change(Bcrypt.add_hash(password)) + |> change(%{ + fever_auth_token: + :crypto.hash(:md5, "#{username}:#{fever_password}") |> Base.encode16(case: :lower) + }) end - - defp put_password_hash(changeset), do: changeset end diff --git a/lib/frenzy_web/controllers/feed_controller.ex b/lib/frenzy_web/controllers/feed_controller.ex index 248fb8e..3ebbcb4 100644 --- a/lib/frenzy_web/controllers/feed_controller.ex +++ b/lib/frenzy_web/controllers/feed_controller.ex @@ -73,7 +73,7 @@ defmodule FrenzyWeb.FeedController do end def refresh(conn, _params) do - feed = conn.assgins[:feed] |> Repo.preload(:filter) + feed = conn.assigns[:feed] |> Repo.preload(:filter) feed = Frenzy.UpdateFeeds.refresh(Frenzy.UpdateFeeds, feed) redirect(conn, to: Routes.feed_path(Endpoint, :show, feed.id)) end diff --git a/lib/frenzy_web/controllers/fever_controller.ex b/lib/frenzy_web/controllers/fever_controller.ex index ee96521..f8cdca2 100644 --- a/lib/frenzy_web/controllers/fever_controller.ex +++ b/lib/frenzy_web/controllers/fever_controller.ex @@ -1,6 +1,6 @@ defmodule FrenzyWeb.FeverController do use FrenzyWeb, :controller - alias Frenzy.{Repo, Group, Feed, Item} + alias Frenzy.{Repo, User, Group, Feed, Item} import Ecto.Query plug :api_check @@ -22,95 +22,133 @@ defmodule FrenzyWeb.FeverController do :invalid -> resp(conn, 401, "Invalid API key") - :ok -> - json(conn, fever_response(params)) + {:ok, user} -> + user = Repo.preload(user, groups: [:feeds]) + res = fever_response(user, params) + json(conn, res) end end defp validate_key(api_key) do - auth = Application.get_env(:frenzy, :auth) - username = auth[:username] - password = auth[:password] - expected = :crypto.hash(:md5, "#{username}:#{password}") |> Base.encode16(case: :lower) + case Repo.get_by(User, fever_auth_token: api_key) do + nil -> + :invalid - case api_key |> String.downcase() do - ^expected -> :ok - _ -> :invalid + user -> + {:ok, user} + end + + # auth = Application.get_env(:frenzy, :auth) + # username = auth[:username] + # password = auth[:password] + # expected = :crypto.hash(:md5, "#{username}:#{password}") |> Base.encode16(case: :lower) + + # case api_key |> String.downcase() do + # ^expected -> :ok + # _ -> :invalid + # end + end + + defp fever_response(user, params) do + %{api_version: 2, auth: 1} + |> mark(user, params) + |> unread_recently_read(user, params) + |> feeds(user, params) + |> groups(user, params) + |> feeds_groups(user, params) + |> favicons(user, params) + |> links(user, params) + |> unread(user, params) + |> saved(user, params) + |> items(user, params) + end + + defp get_user_item(user, id) do + item = Repo.get(Item, id) |> Repo.preload(:feed) + + if not is_nil(item) do + feeds = Enum.flat_map(user.groups, fn g -> g.feeds end) + + if Enum.any?(feeds, fn f -> f.id == item.feed_id end) do + {:ok, item} + else + {:error, "item does not belong to given user"} + end + else + {:error, "item does not exist"} end end - defp fever_response(params) do - %{api_version: 2, auth: 1} - |> mark(params) - |> unread_recently_read(params) - |> feeds(params) - |> groups(params) - |> feeds_groups(params) - |> favicons(params) - |> links(params) - |> unread(params) - |> saved(params) - |> items(params) - end + defp mark(res, user, %{"mark" => "item", "id" => id, "as" => as}) do + with {:ok, item} <- get_user_item(user, id) do + diff = + case as do + "read" -> + %{read: true, read_date: Timex.now()} - defp mark(res, %{"mark" => "item", "id" => id, "as" => as}) do - item = Repo.get(Item, id) |> Repo.preload(:feed) + "unread" -> + %{read: false, read_date: nil} - diff = - case as do - "read" -> - %{read: true, read_date: Timex.now()} + _ -> + %{} + end - "unread" -> - %{read: false, read_date: nil} + changeset = Item.changeset(item, diff) + Repo.update(changeset) + end - _ -> - %{} - end - - changeset = Item.changeset(item, diff) - Repo.update(changeset) res end - defp mark(res, _), do: res + defp mark(res, _, _), do: res - defp unread_recently_read(res, %{"unread_recently_read" => 1}) do - Repo.all(from i in Item, where: i.read, where: i.read_date >= from_now(-1, "hour")) + defp unread_recently_read(res, user, %{"unread_recently_read" => 1}) do + feed_ids = + user.groups + |> Enum.flat_map(fn g -> g.feeds end) + |> Enum.map(fn f -> f.id end) + + Repo.all( + from i in Item, + where: i.feed_id in ^feed_ids, + where: i.read, + where: i.read_date >= from_now(-1, "hour") + ) |> Enum.map(fn i -> Item.changeset(i, %{read: false, read_date: nil}) end) |> Enum.map(&Repo.update/1) res end - defp unread_recently_read(res, _), do: res + defp unread_recently_read(res, _, _), do: res - defp feeds(res, %{"feeds" => _}) do + defp feeds(res, user, %{"feeds" => _}) do feeds = - Repo.all(Feed) + user.groups + |> Enum.flat_map(fn g -> g.feeds end) |> Enum.map(&Feed.to_fever/1) res |> Map.put(:feeds, feeds) end - defp feeds(res, _), do: res + defp feeds(res, _, _), do: res - defp groups(res, %{"groups" => _}) do + defp groups(res, user, %{"groups" => _}) do groups = - Repo.all(Group) + user.groups |> Enum.map(&Group.to_fever_group/1) res |> Map.put(:groups, groups) end - defp groups(res, _), do: res + defp groups(res, _, _), do: res - defp feeds_groups(res, params) do + defp feeds_groups(res, user, params) do if Map.has_key?(params, "feeds") or Map.has_key?(params, "groups") do feeds_groups = - Repo.all(from Group, preload: [:feeds]) + user.groups |> Enum.map(&Group.to_fever_feeds_group/1) res @@ -120,23 +158,28 @@ defmodule FrenzyWeb.FeverController do end end - defp favicons(res, %{"favicons" => _}) do + defp favicons(res, _user, %{"favicons" => _}) do res |> Map.put(:favicons, []) end - defp favicons(res, _), do: res + defp favicons(res, _, _), do: res - defp links(res, %{"links" => _}) do + defp links(res, _user, %{"links" => _}) do res |> Map.put(:links, []) end - defp links(res, _), do: res + defp links(res, _, _), do: res + + defp unread(res, user, %{"unread_item_ids" => _}) do + feed_ids = + user.groups + |> Enum.flat_map(fn g -> g.feeds end) + |> Enum.map(fn f -> f.id end) - defp unread(res, %{"unread_item_ids" => _}) do unread = - Repo.all(from Item, where: [read: false]) + Repo.all(from i in Item, where: i.feed_id in ^feed_ids, where: [read: false]) |> Enum.map(fn item -> item.id end) |> Enum.join(",") @@ -144,16 +187,21 @@ defmodule FrenzyWeb.FeverController do |> Map.put(:unread_item_ids, unread) end - defp unread(res, _), do: res + defp unread(res, _, _), do: res - defp saved(res, %{"saved_item_ids" => _}) do + defp saved(res, _user, %{"saved_item_ids" => _}) do res |> Map.put(:saved_item_ids, "") end - defp saved(res, _), do: res + defp saved(res, _, _), do: res + + defp items(res, user, %{"items" => _} = params) do + feed_ids = + user.groups + |> Enum.flat_map(fn g -> g.feeds end) + |> Enum.map(fn f -> f.id end) - defp items(res, %{"items" => _} = params) do items = cond do Map.has_key?(params, "with_ids") -> @@ -161,8 +209,15 @@ defmodule FrenzyWeb.FeverController do |> String.split(",") |> Enum.map(fn id -> {id, _} = id |> String.trim() |> Integer.parse() - Repo.get(Item, id) + item = Repo.get(Item, id) + + if item.feed_id in feed_ids do + item + else + nil + end end) + |> Enum.reject(&is_nil/1) Map.has_key?(params, "since_id") -> since = Repo.get(Item, params["since_id"]) @@ -170,6 +225,7 @@ defmodule FrenzyWeb.FeverController do Repo.all( from i in Item, + where: i.feed_id in ^feed_ids, where: i.inserted_at > ^since.inserted_at, order_by: [asc: :id], limit: 50 @@ -181,6 +237,7 @@ defmodule FrenzyWeb.FeverController do Repo.all( from i in Item, + where: i.feed_id in ^feed_ids, where: i.inserted_at < ^max.inserted_at, order_by: [desc: :id], limit: 50 @@ -201,5 +258,5 @@ defmodule FrenzyWeb.FeverController do |> Map.put(:total_items, Enum.count(items)) end - defp items(res, _), do: res + defp items(res, _, _), do: res end diff --git a/lib/mix/tasks/frenzy/user.ex b/lib/mix/tasks/frenzy/user.ex index ac38b41..3d7565e 100644 --- a/lib/mix/tasks/frenzy/user.ex +++ b/lib/mix/tasks/frenzy/user.ex @@ -6,11 +6,13 @@ defmodule Mix.Tasks.Frenzy.User do def run(["add"]) do username = IO.gets("Username: ") |> String.trim() password = IO.gets("Password: ") |> String.trim() + fever_password = IO.gets("Fever Password: ") |> String.trim() changeset = User.registration_changeset(%User{}, %{ username: username, - password: password + password: password, + fever_password: fever_password }) Mix.Task.run("app.start") diff --git a/priv/repo/migrations/20190324143311_users_add_fever_token.exs b/priv/repo/migrations/20190324143311_users_add_fever_token.exs new file mode 100644 index 0000000..abe34d6 --- /dev/null +++ b/priv/repo/migrations/20190324143311_users_add_fever_token.exs @@ -0,0 +1,9 @@ +defmodule Frenzy.Repo.Migrations.UsersAddFeverToken do + use Ecto.Migration + + def change do + alter table(:users) do + add :fever_auth_token, :string + end + end +end