Compare commits
4 Commits
eb0ebeba91
...
9fa6968f6f
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 9fa6968f6f | |
Shadowfacts | 74a82967dd | |
Shadowfacts | 762675b38c | |
Shadowfacts | 29411a9d08 |
|
@ -2,9 +2,25 @@ defmodule Frenzy.Group do
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
def to_fever_group(group) do
|
||||||
|
%{
|
||||||
|
id: group.id,
|
||||||
|
title: group.title
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_fever_feeds_group(group) do
|
||||||
|
%{
|
||||||
|
group_id: group.id,
|
||||||
|
feed_ids: group.feeds |> Enum.map(fn feed -> feed.id end) |> Enum.join(",")
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
schema "groups" do
|
schema "groups" do
|
||||||
field :title, :string
|
field :title, :string
|
||||||
|
|
||||||
|
belongs_to :user, Frenzy.User
|
||||||
|
|
||||||
has_many :feeds, Frenzy.Feed, on_delete: :delete_all
|
has_many :feeds, Frenzy.Feed, on_delete: :delete_all
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
|
|
|
@ -32,9 +32,9 @@ defmodule Frenzy.UpdateFeeds do
|
||||||
|
|
||||||
defp schedule_update() do
|
defp schedule_update() do
|
||||||
# 15 minutes
|
# 15 minutes
|
||||||
# Process.send_after(self(), :update_feeds, 15 * 60 * 1000)
|
Process.send_after(self(), :update_feeds, 15 * 60 * 1000)
|
||||||
# 1 minutes
|
# 1 minutes
|
||||||
Process.send_after(self(), :update_feeds, 60 * 1000)
|
# Process.send_after(self(), :update_feeds, 60 * 1000)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp update_feeds() do
|
defp update_feeds() do
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
defmodule Frenzy.User do
|
||||||
|
use Ecto.Schema
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
schema "users" 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
|
||||||
|
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def changeset(user, attrs) do
|
||||||
|
user
|
||||||
|
|> cast(attrs, [:username, :password_hash])
|
||||||
|
|> validate_required([:username, :password_hash])
|
||||||
|
end
|
||||||
|
|
||||||
|
def registration_changeset(user, attrs) do
|
||||||
|
user
|
||||||
|
|> 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: %{username: username, password: password, fever_password: fever_password}
|
||||||
|
} = changeset
|
||||||
|
) do
|
||||||
|
changeset
|
||||||
|
|> change(Bcrypt.add_hash(password))
|
||||||
|
|> change(%{
|
||||||
|
fever_auth_token:
|
||||||
|
:crypto.hash(:md5, "#{username}:#{fever_password}") |> Base.encode16(case: :lower)
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
|
@ -5,8 +5,28 @@ defmodule FrenzyWeb.FeedController do
|
||||||
alias FrenzyWeb.Endpoint
|
alias FrenzyWeb.Endpoint
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
|
plug :user_owns_feed
|
||||||
|
|
||||||
|
defp user_owns_feed(%Plug.Conn{path_params: %{"id" => id}} = conn, _opts) do
|
||||||
|
user = conn.assigns[:user]
|
||||||
|
|
||||||
|
feed = Repo.get(Feed, id)
|
||||||
|
|
||||||
|
if Enum.any?(user.groups, fn g -> g.id == feed.group_id end) do
|
||||||
|
conn
|
||||||
|
|> assign(:feed, feed)
|
||||||
|
else
|
||||||
|
conn
|
||||||
|
|> put_flash(:error, "You do not have permission to access that resource.")
|
||||||
|
|> redirect(to: Routes.group_path(Endpoint, :index))
|
||||||
|
|> halt()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp user_owns_feed(conn, _opts), do: conn
|
||||||
|
|
||||||
def show(conn, %{"id" => id}) do
|
def show(conn, %{"id" => id}) do
|
||||||
feed = Repo.get(Feed, id) |> Repo.preload(:filter)
|
feed = conn.assigns[:feed]
|
||||||
items = Repo.all(from Item, where: [feed_id: ^id, tombstone: false], order_by: [desc: :date])
|
items = Repo.all(from Item, where: [feed_id: ^id, tombstone: false], order_by: [desc: :date])
|
||||||
|
|
||||||
render(conn, "show.html", %{
|
render(conn, "show.html", %{
|
||||||
|
@ -32,28 +52,28 @@ defmodule FrenzyWeb.FeedController do
|
||||||
redirect(conn, to: Routes.group_path(Endpoint, :show, group_id))
|
redirect(conn, to: Routes.group_path(Endpoint, :show, group_id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete(conn, %{"id" => id}) do
|
def delete(conn, _params) do
|
||||||
feed = Repo.get(Feed, id)
|
feed = conn.assigns[:feed]
|
||||||
{:ok, _} = Repo.delete(feed)
|
{:ok, _} = Repo.delete(feed)
|
||||||
redirect(conn, to: Routes.group_path(Endpoint, :show, feed.group_id))
|
redirect(conn, to: Routes.group_path(Endpoint, :show, feed.group_id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def enable_filter(conn, %{"id" => id}) do
|
def enable_filter(conn, _params) do
|
||||||
feed = Repo.get(Feed, id) |> Repo.preload(:filter)
|
feed = conn.assigns[:feed] |> Repo.preload(:filter)
|
||||||
changeset = Feed.changeset(feed, %{filter_enabled: true})
|
changeset = Feed.changeset(feed, %{filter_enabled: true})
|
||||||
Repo.update(changeset)
|
Repo.update(changeset)
|
||||||
redirect(conn, to: Routes.feed_path(Endpoint, :show, id))
|
redirect(conn, to: Routes.feed_path(Endpoint, :show, feed.id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def disable_filter(conn, %{"id" => id}) do
|
def disable_filter(conn, _params) do
|
||||||
feed = Repo.get(Feed, id) |> Repo.preload(:filter)
|
feed = conn.assigns[:feed] |> Repo.preload(:filter)
|
||||||
changeset = Feed.changeset(feed, %{filter_enabled: false})
|
changeset = Feed.changeset(feed, %{filter_enabled: false})
|
||||||
Repo.update(changeset)
|
Repo.update(changeset)
|
||||||
redirect(conn, to: Routes.feed_path(Endpoint, :show, id))
|
redirect(conn, to: Routes.feed_path(Endpoint, :show, feed.id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def refresh(conn, %{"id" => id}) do
|
def refresh(conn, _params) do
|
||||||
feed = Repo.get(Feed, id) |> Repo.preload(:filter)
|
feed = conn.assigns[:feed] |> Repo.preload(:filter)
|
||||||
feed = Frenzy.UpdateFeeds.refresh(Frenzy.UpdateFeeds, feed)
|
feed = Frenzy.UpdateFeeds.refresh(Frenzy.UpdateFeeds, feed)
|
||||||
redirect(conn, to: Routes.feed_path(Endpoint, :show, feed.id))
|
redirect(conn, to: Routes.feed_path(Endpoint, :show, feed.id))
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule FrenzyWeb.FeverController do
|
defmodule FrenzyWeb.FeverController do
|
||||||
use FrenzyWeb, :controller
|
use FrenzyWeb, :controller
|
||||||
alias Frenzy.{Repo, Feed, Item}
|
alias Frenzy.{Repo, User, Group, Feed, Item}
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
plug :api_check
|
plug :api_check
|
||||||
|
@ -13,7 +13,7 @@ defmodule FrenzyWeb.FeverController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def get(conn, params) do
|
def get(conn, _params) do
|
||||||
json(conn, %{api_version: 2, auth: 0})
|
json(conn, %{api_version: 2, auth: 0})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -22,106 +22,164 @@ defmodule FrenzyWeb.FeverController do
|
||||||
:invalid ->
|
:invalid ->
|
||||||
resp(conn, 401, "Invalid API key")
|
resp(conn, 401, "Invalid API key")
|
||||||
|
|
||||||
:ok ->
|
{:ok, user} ->
|
||||||
json(conn, fever_response(params))
|
user = Repo.preload(user, groups: [:feeds])
|
||||||
|
res = fever_response(user, params)
|
||||||
|
json(conn, res)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp validate_key(api_key) do
|
defp validate_key(api_key) do
|
||||||
auth = Application.get_env(:frenzy, :auth)
|
case Repo.get_by(User, fever_auth_token: api_key) do
|
||||||
username = auth[:username]
|
nil ->
|
||||||
password = auth[:password]
|
:invalid
|
||||||
expected = :crypto.hash(:md5, "#{username}:#{password}") |> Base.encode16(case: :lower)
|
|
||||||
|
|
||||||
case api_key |> String.downcase() do
|
user ->
|
||||||
^expected -> :ok
|
{:ok, user}
|
||||||
_ -> :invalid
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
defp fever_response(params) do
|
defp mark(res, user, %{"mark" => "item", "id" => id, "as" => as}) do
|
||||||
res =
|
with {:ok, item} <- get_user_item(user, id) do
|
||||||
%{api_version: 2, auth: 1}
|
diff =
|
||||||
|> mark(params)
|
case as do
|
||||||
|> unread_recently_read(params)
|
"read" ->
|
||||||
|> groups(params)
|
%{read: true, read_date: Timex.now()}
|
||||||
|> feeds(params)
|
|
||||||
|> favicons(params)
|
|
||||||
|> links(params)
|
|
||||||
|> unread(params)
|
|
||||||
|> saved(params)
|
|
||||||
|> items(params)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp mark(res, %{"mark" => "item", "id" => id, "as" => as} = params) do
|
"unread" ->
|
||||||
item = Repo.get(Item, id) |> Repo.preload(:feed)
|
%{read: false, read_date: nil}
|
||||||
|
|
||||||
diff =
|
_ ->
|
||||||
case as do
|
%{}
|
||||||
"read" ->
|
end
|
||||||
%{read: true, read_date: Timex.now()}
|
|
||||||
|
|
||||||
"unread" ->
|
changeset = Item.changeset(item, diff)
|
||||||
%{read: false, read_date: nil}
|
Repo.update(changeset)
|
||||||
|
end
|
||||||
|
|
||||||
_ ->
|
|
||||||
%{}
|
|
||||||
end
|
|
||||||
|
|
||||||
changeset = Item.changeset(item, diff)
|
|
||||||
Repo.update(changeset)
|
|
||||||
res
|
res
|
||||||
end
|
end
|
||||||
|
|
||||||
defp mark(res, _), do: res
|
defp mark(res, _, _), do: res
|
||||||
|
|
||||||
defp unread_recently_read(res, %{"unread_recently_read" => 1}) do
|
defp unread_recently_read(res, user, %{"unread_recently_read" => 1}) do
|
||||||
Repo.all(from i in Item, where: i.read, where: i.read_date >= from_now(-1, "hour"))
|
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(fn i -> Item.changeset(i, %{read: false, read_date: nil}) end)
|
||||||
|> Enum.map(&Repo.update/1)
|
|> Enum.map(&Repo.update/1)
|
||||||
|
|
||||||
res
|
res
|
||||||
end
|
end
|
||||||
|
|
||||||
defp unread_recently_read(res, _), do: res
|
defp unread_recently_read(res, _, _), do: res
|
||||||
|
|
||||||
defp groups(res, %{"groups" => _}) do
|
defp feeds(res, user, %{"feeds" => _}) do
|
||||||
res
|
|
||||||
|> Map.put(:groups, [])
|
|
||||||
|> Map.put(:feeds_groups, [])
|
|
||||||
end
|
|
||||||
|
|
||||||
defp groups(res, _), do: res
|
|
||||||
|
|
||||||
defp feeds(res, %{"feeds" => _}) do
|
|
||||||
feeds =
|
feeds =
|
||||||
Repo.all(Feed)
|
user.groups
|
||||||
|
|> Enum.flat_map(fn g -> g.feeds end)
|
||||||
|> Enum.map(&Feed.to_fever/1)
|
|> Enum.map(&Feed.to_fever/1)
|
||||||
|
|
||||||
res
|
res
|
||||||
|> Map.put(:feeds, feeds)
|
|> Map.put(:feeds, feeds)
|
||||||
|> Map.put(:feeds_groups, [])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp feeds(res, _), do: res
|
defp feeds(res, _, _), do: res
|
||||||
|
|
||||||
defp favicons(res, %{"favicons" => _}) do
|
defp groups(res, user, %{"groups" => _}) do
|
||||||
|
groups =
|
||||||
|
user.groups
|
||||||
|
|> Enum.map(&Group.to_fever_group/1)
|
||||||
|
|
||||||
|
res
|
||||||
|
|> Map.put(:groups, groups)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp groups(res, _, _), do: res
|
||||||
|
|
||||||
|
defp feeds_groups(res, user, params) do
|
||||||
|
if Map.has_key?(params, "feeds") or Map.has_key?(params, "groups") do
|
||||||
|
feeds_groups =
|
||||||
|
user.groups
|
||||||
|
|> Enum.map(&Group.to_fever_feeds_group/1)
|
||||||
|
|
||||||
|
res
|
||||||
|
|> Map.put(:feeds_groups, feeds_groups)
|
||||||
|
else
|
||||||
|
res
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp favicons(res, _user, %{"favicons" => _}) do
|
||||||
res
|
res
|
||||||
|> Map.put(:favicons, [])
|
|> Map.put(:favicons, [])
|
||||||
end
|
end
|
||||||
|
|
||||||
defp favicons(res, _), do: res
|
defp favicons(res, _, _), do: res
|
||||||
|
|
||||||
defp links(res, %{"links" => _}) do
|
defp links(res, _user, %{"links" => _}) do
|
||||||
res
|
res
|
||||||
|> Map.put(:links, [])
|
|> Map.put(:links, [])
|
||||||
end
|
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 =
|
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.map(fn item -> item.id end)
|
||||||
|> Enum.join(",")
|
|> Enum.join(",")
|
||||||
|
|
||||||
|
@ -129,16 +187,21 @@ defmodule FrenzyWeb.FeverController do
|
||||||
|> Map.put(:unread_item_ids, unread)
|
|> Map.put(:unread_item_ids, unread)
|
||||||
end
|
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
|
res
|
||||||
|> Map.put(:saved_item_ids, "")
|
|> Map.put(:saved_item_ids, "")
|
||||||
end
|
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 =
|
items =
|
||||||
cond do
|
cond do
|
||||||
Map.has_key?(params, "with_ids") ->
|
Map.has_key?(params, "with_ids") ->
|
||||||
|
@ -146,8 +209,15 @@ defmodule FrenzyWeb.FeverController do
|
||||||
|> String.split(",")
|
|> String.split(",")
|
||||||
|> Enum.map(fn id ->
|
|> Enum.map(fn id ->
|
||||||
{id, _} = id |> String.trim() |> Integer.parse()
|
{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)
|
end)
|
||||||
|
|> Enum.reject(&is_nil/1)
|
||||||
|
|
||||||
Map.has_key?(params, "since_id") ->
|
Map.has_key?(params, "since_id") ->
|
||||||
since = Repo.get(Item, params["since_id"])
|
since = Repo.get(Item, params["since_id"])
|
||||||
|
@ -155,6 +225,7 @@ defmodule FrenzyWeb.FeverController do
|
||||||
|
|
||||||
Repo.all(
|
Repo.all(
|
||||||
from i in Item,
|
from i in Item,
|
||||||
|
where: i.feed_id in ^feed_ids,
|
||||||
where: i.inserted_at > ^since.inserted_at,
|
where: i.inserted_at > ^since.inserted_at,
|
||||||
order_by: [asc: :id],
|
order_by: [asc: :id],
|
||||||
limit: 50
|
limit: 50
|
||||||
|
@ -166,6 +237,7 @@ defmodule FrenzyWeb.FeverController do
|
||||||
|
|
||||||
Repo.all(
|
Repo.all(
|
||||||
from i in Item,
|
from i in Item,
|
||||||
|
where: i.feed_id in ^feed_ids,
|
||||||
where: i.inserted_at < ^max.inserted_at,
|
where: i.inserted_at < ^max.inserted_at,
|
||||||
order_by: [desc: :id],
|
order_by: [desc: :id],
|
||||||
limit: 50
|
limit: 50
|
||||||
|
@ -178,6 +250,7 @@ defmodule FrenzyWeb.FeverController do
|
||||||
items =
|
items =
|
||||||
items
|
items
|
||||||
|> Enum.reject(&is_nil/1)
|
|> Enum.reject(&is_nil/1)
|
||||||
|
|> Enum.reject(fn item -> item.tombstone end)
|
||||||
|> Enum.map(&Item.to_fever/1)
|
|> Enum.map(&Item.to_fever/1)
|
||||||
|
|
||||||
res
|
res
|
||||||
|
@ -185,5 +258,5 @@ defmodule FrenzyWeb.FeverController do
|
||||||
|> Map.put(:total_items, Enum.count(items))
|
|> Map.put(:total_items, Enum.count(items))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp items(res, _), do: res
|
defp items(res, _, _), do: res
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,20 +3,39 @@ defmodule FrenzyWeb.GroupController do
|
||||||
alias Frenzy.{Repo, Group, Feed}
|
alias Frenzy.{Repo, Group, Feed}
|
||||||
alias FrenzyWeb.Router.Helpers, as: Routes
|
alias FrenzyWeb.Router.Helpers, as: Routes
|
||||||
alias FrenzyWeb.Endpoint
|
alias FrenzyWeb.Endpoint
|
||||||
import Ecto.Query
|
|
||||||
|
plug :user_owns_group
|
||||||
|
|
||||||
|
defp user_owns_group(%Plug.Conn{path_params: %{"id" => id}} = conn, _opts) do
|
||||||
|
user = conn.assigns[:user]
|
||||||
|
|
||||||
|
group = Repo.get(Group, id)
|
||||||
|
|
||||||
|
if Enum.any?(user.groups, fn g -> g.id == group.id end) do
|
||||||
|
conn
|
||||||
|
|> assign(:group, group)
|
||||||
|
else
|
||||||
|
conn
|
||||||
|
|> put_flash(:error, "You do not have permission to acess that resource.")
|
||||||
|
|> redirect(to: Routes.group_path(Endpoint, :index))
|
||||||
|
|> halt()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp user_owns_group(conn, _opts), do: conn
|
||||||
|
|
||||||
def index(conn, _params) do
|
def index(conn, _params) do
|
||||||
groups = Repo.all(from group in Group, preload: [:feeds])
|
groups = conn.assigns[:user].groups
|
||||||
render(conn, "index.html", groups: groups)
|
render(conn, "index.html", groups: groups)
|
||||||
end
|
end
|
||||||
|
|
||||||
def show(conn, %{"id" => id}) do
|
def show(conn, _params) do
|
||||||
group = Repo.get(Group, id) |> Repo.preload(:feeds)
|
group = conn.assigns[:group] |> Repo.preload(:feeds)
|
||||||
|
|
||||||
create_feed_changeset =
|
create_feed_changeset =
|
||||||
Feed.changeset(
|
Feed.changeset(
|
||||||
%Feed{
|
%Feed{
|
||||||
group_id: id
|
group_id: group.id
|
||||||
},
|
},
|
||||||
%{}
|
%{}
|
||||||
)
|
)
|
||||||
|
@ -34,15 +53,21 @@ defmodule FrenzyWeb.GroupController do
|
||||||
end
|
end
|
||||||
|
|
||||||
def create(conn, %{"group" => %{"title" => title}}) do
|
def create(conn, %{"group" => %{"title" => title}}) do
|
||||||
changeset = Group.changeset(%Group{title: title}, %{})
|
user = conn.assigns[:user]
|
||||||
|
|
||||||
|
changeset =
|
||||||
|
Ecto.build_assoc(user, :groups, %{
|
||||||
|
title: title
|
||||||
|
})
|
||||||
|
|
||||||
{:ok, group} = Repo.insert(changeset)
|
{:ok, group} = Repo.insert(changeset)
|
||||||
|
|
||||||
redirect(conn, to: Routes.group_path(Endpoint, :index))
|
redirect(conn, to: Routes.group_path(Endpoint, :show, group.id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete(conn, %{"id" => id}) do
|
def delete(conn, _params) do
|
||||||
group = Repo.get(Group, id)
|
group = conn.assigns[:group]
|
||||||
|
|
||||||
{:ok, _} = Repo.delete(group)
|
{:ok, _} = Repo.delete(group)
|
||||||
redirect(conn, to: Routes.group_path(Endpoint, :index))
|
redirect(conn, to: Routes.group_path(Endpoint, :index))
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,33 +4,60 @@ defmodule FrenzyWeb.ItemController do
|
||||||
alias FrenzyWeb.Router.Helpers, as: Routes
|
alias FrenzyWeb.Router.Helpers, as: Routes
|
||||||
alias FrenzyWeb.Endpoint
|
alias FrenzyWeb.Endpoint
|
||||||
|
|
||||||
def show(conn, %{"id" => id}) do
|
plug :user_owns_item
|
||||||
|
|
||||||
|
defp user_owns_item(%Plug.Conn{path_params: %{"id" => id}} = conn, _opts) do
|
||||||
|
user = conn.assigns[:user]
|
||||||
|
|
||||||
item = Repo.get(Item, id)
|
item = Repo.get(Item, id)
|
||||||
feed = Repo.get(Feed, item.feed_id)
|
|
||||||
|
feeds = Enum.flat_map(user.groups, fn g -> g.feeds end)
|
||||||
|
|
||||||
|
if Enum.any?(feeds, fn f -> f.id == item.feed_id end) do
|
||||||
|
conn
|
||||||
|
|> assign(:item, item)
|
||||||
|
else
|
||||||
|
conn
|
||||||
|
|> put_flash(:error, "You do not have permission to access that resource.")
|
||||||
|
|> redirect(to: Routes.group_path(Endpoint, :index))
|
||||||
|
|> halt()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp user_owns_item(conn, _opts), do: conn
|
||||||
|
|
||||||
|
def show(conn, _params) do
|
||||||
|
item = conn.assigns[:item] |> Repo.preload(:feed)
|
||||||
|
|
||||||
render(conn, "show.html", %{
|
render(conn, "show.html", %{
|
||||||
item: item,
|
item: item,
|
||||||
feed: feed
|
feed: item.feed
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
def read(conn, %{"id" => id}) do
|
def read(conn, _params) do
|
||||||
item = Repo.get(Item, id) |> Repo.preload(:feed)
|
item = conn.assigns[:item] |> Repo.preload(:feed)
|
||||||
changeset = Item.changeset(item, %{
|
|
||||||
read: true,
|
changeset =
|
||||||
read_date: Timex.now
|
Item.changeset(item, %{
|
||||||
|
read: true,
|
||||||
|
read_date: Timex.now()
|
||||||
})
|
})
|
||||||
|
|
||||||
Repo.update(changeset)
|
Repo.update(changeset)
|
||||||
redirect(conn, to: Routes.item_path(Endpoint, :show, id))
|
redirect(conn, to: Routes.item_path(Endpoint, :show, item.id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def unread(conn, %{"id" => id}) do
|
def unread(conn, _params) do
|
||||||
item = Repo.get(Item, id) |> Repo.preload(:feed)
|
item = conn.assigns[:item] |> Repo.preload(:feed)
|
||||||
changeset = Item.changeset(item, %{
|
|
||||||
read: false,
|
changeset =
|
||||||
read_date: nil
|
Item.changeset(item, %{
|
||||||
|
read: false,
|
||||||
|
read_date: nil
|
||||||
})
|
})
|
||||||
Repo.update(changeset)
|
|
||||||
redirect(conn, to: Routes.item_path(Endpoint, :show, id))
|
|
||||||
end
|
|
||||||
|
|
||||||
|
Repo.update(changeset)
|
||||||
|
redirect(conn, to: Routes.item_path(Endpoint, :show, item.id))
|
||||||
|
end
|
||||||
end
|
end
|
|
@ -0,0 +1,31 @@
|
||||||
|
defmodule FrenzyWeb.LoginController do
|
||||||
|
use FrenzyWeb, :controller
|
||||||
|
alias Frenzy.{Repo, User}
|
||||||
|
alias FrenzyWeb.Router.Helpers, as: Routes
|
||||||
|
alias FrenzyWeb.Endpoint
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
|
def login(conn, _params) do
|
||||||
|
render(conn, "login.html")
|
||||||
|
end
|
||||||
|
|
||||||
|
@error_message "Invalid username or password"
|
||||||
|
|
||||||
|
def login_post(conn, %{"username" => username, "password" => password}) do
|
||||||
|
user = Repo.get_by(User, username: username)
|
||||||
|
|
||||||
|
case Bcrypt.check_pass(user, password) do
|
||||||
|
{:ok, user} ->
|
||||||
|
user_token = Phoenix.Token.sign(Endpoint, "user token", user.id)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_session(:user_token, user_token)
|
||||||
|
|> redirect(to: Routes.group_path(Endpoint, :index))
|
||||||
|
|
||||||
|
{:error, _reason} ->
|
||||||
|
conn
|
||||||
|
|> put_flash(:error, @error_message)
|
||||||
|
|> redirect(to: Routes.login_path(Endpoint, :login))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,31 @@
|
||||||
|
defmodule FrenzyWeb.Plug.Authenticate do
|
||||||
|
import Plug.Conn
|
||||||
|
alias Frenzy.{Repo, User}
|
||||||
|
alias FrenzyWeb.Router.Helpers, as: Routes
|
||||||
|
alias FrenzyWeb.Endpoint
|
||||||
|
|
||||||
|
def init(opts), do: opts
|
||||||
|
|
||||||
|
def call(conn, _opts) do
|
||||||
|
user_token = get_session(conn, :user_token)
|
||||||
|
|
||||||
|
case Phoenix.Token.verify(Endpoint, "user token", user_token, max_age: 24 * 60 * 60) do
|
||||||
|
{:error, _reason} ->
|
||||||
|
conn
|
||||||
|
|> Phoenix.Controller.redirect(to: Routes.login_path(Endpoint, :login))
|
||||||
|
|> halt()
|
||||||
|
|
||||||
|
{:ok, user_id} ->
|
||||||
|
case Repo.get(User, user_id) do
|
||||||
|
nil ->
|
||||||
|
conn
|
||||||
|
|> Phoenix.Controller.redirect(to: Routes.login_path(Endpoint, :login))
|
||||||
|
|> halt()
|
||||||
|
|
||||||
|
user ->
|
||||||
|
user = Repo.preload(user, groups: [:feeds])
|
||||||
|
assign(conn, :user, user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -7,7 +7,15 @@ defmodule FrenzyWeb.Router do
|
||||||
plug :fetch_flash
|
plug :fetch_flash
|
||||||
plug :protect_from_forgery
|
plug :protect_from_forgery
|
||||||
plug :put_secure_browser_headers
|
plug :put_secure_browser_headers
|
||||||
plug BasicAuth, use_config: {:frenzy, :auth}
|
end
|
||||||
|
|
||||||
|
pipeline :authenticate do
|
||||||
|
# plug :accepts, ["html"]
|
||||||
|
# plug :fetch_session
|
||||||
|
# plug :fetch_flash
|
||||||
|
# plug :protect_from_forgery
|
||||||
|
# plug :put_secure_browser_headers
|
||||||
|
plug FrenzyWeb.Plug.Authenticate
|
||||||
end
|
end
|
||||||
|
|
||||||
pipeline :api do
|
pipeline :api do
|
||||||
|
@ -17,6 +25,14 @@ defmodule FrenzyWeb.Router do
|
||||||
scope "/", FrenzyWeb do
|
scope "/", FrenzyWeb do
|
||||||
pipe_through :browser
|
pipe_through :browser
|
||||||
|
|
||||||
|
get "/login", LoginController, :login
|
||||||
|
post "/login", LoginController, :login_post
|
||||||
|
end
|
||||||
|
|
||||||
|
scope "/", FrenzyWeb do
|
||||||
|
pipe_through :browser
|
||||||
|
pipe_through :authenticate
|
||||||
|
|
||||||
get "/", GroupController, :index
|
get "/", GroupController, :index
|
||||||
resources "/groups", GroupController, except: [:edit, :update]
|
resources "/groups", GroupController, except: [:edit, :update]
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<%= text_input form, :feed_url %>
|
<%= text_input form, :feed_url %>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<%= submit "Create Feed" %>
|
<%= submit "Add Feed" %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
<% IO.inspect(get_flash(@conn, :error)) %>
|
||||||
|
|
||||||
|
<%= form_tag Routes.login_path(@conn, :login_post), method: :post do %>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input type="text" name="username" id="username">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" name="password" id="password">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<%= submit "Log In" %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
|
@ -0,0 +1,3 @@
|
||||||
|
defmodule FrenzyWeb.LoginView do
|
||||||
|
use FrenzyWeb, :view
|
||||||
|
end
|
|
@ -0,0 +1,28 @@
|
||||||
|
defmodule Mix.Tasks.Frenzy.User do
|
||||||
|
use Mix.Task
|
||||||
|
alias Frenzy.{Repo, User}
|
||||||
|
|
||||||
|
@shortdoc "Adds a new user from the given username and password."
|
||||||
|
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,
|
||||||
|
fever_password: fever_password
|
||||||
|
})
|
||||||
|
|
||||||
|
Mix.Task.run("app.start")
|
||||||
|
|
||||||
|
{:ok, _user} = Repo.insert(changeset)
|
||||||
|
|
||||||
|
IO.puts("User #{username} successfully created")
|
||||||
|
end
|
||||||
|
|
||||||
|
def run(_) do
|
||||||
|
IO.puts("Invalid usage, expected is mix frenzy.user add")
|
||||||
|
end
|
||||||
|
end
|
2
mix.exs
2
mix.exs
|
@ -47,7 +47,7 @@ defmodule Frenzy.MixProject do
|
||||||
{:fiet, git: "https://github.com/shadowfacts/fiet.git", branch: "master"},
|
{:fiet, git: "https://github.com/shadowfacts/fiet.git", branch: "master"},
|
||||||
{:timex, "~> 3.0"},
|
{:timex, "~> 3.0"},
|
||||||
{:readability, git: "https://github.com/shadowfacts/readability.git", branch: "master"},
|
{:readability, git: "https://github.com/shadowfacts/readability.git", branch: "master"},
|
||||||
{:basic_auth, "~> 2.2.2"}
|
{:bcrypt_elixir, "~> 2.0"}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
3
mix.lock
3
mix.lock
|
@ -1,7 +1,9 @@
|
||||||
%{
|
%{
|
||||||
"basic_auth": {:hex, :basic_auth, "2.2.4", "d8c748237870dd1df3bc5c0f1ab4f1fad6270c75472d7e62b19302ec59e92a79", [:mix], [{:plug, "~> 0.14 or ~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
|
"basic_auth": {:hex, :basic_auth, "2.2.4", "d8c748237870dd1df3bc5c0f1ab4f1fad6270c75472d7e62b19302ec59e92a79", [:mix], [{:plug, "~> 0.14 or ~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
|
"bcrypt_elixir": {:hex, :bcrypt_elixir, "2.0.1", "1061e2114aaac554c12e5c1e4608bf4aadaca839f30d1b85224272facd5e6427", [:make, :mix], [{:comeonin, "~> 5.1", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
"certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
|
"certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"},
|
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"},
|
||||||
|
"comeonin": {:hex, :comeonin, "5.1.1", "0abd6bae41acc01c369bb3eafe46399f301bf4e1bacebafdb89252bbb8a1a32d", [:mix], [], "hexpm"},
|
||||||
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},
|
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},
|
||||||
"cowboy": {:hex, :cowboy, "2.6.0", "dc1ff5354c89e36a3e3ef8d10433396dcff0dcbb1d4223b58c64c2d51a6d88d9", [:rebar3], [{:cowlib, "~> 2.7.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
|
"cowboy": {:hex, :cowboy, "2.6.0", "dc1ff5354c89e36a3e3ef8d10433396dcff0dcbb1d4223b58c64c2d51a6d88d9", [:rebar3], [{:cowlib, "~> 2.7.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
"cowlib": {:hex, :cowlib, "2.7.0", "3ef16e77562f9855a2605900cedb15c1462d76fb1be6a32fc3ae91973ee543d2", [:rebar3], [], "hexpm"},
|
"cowlib": {:hex, :cowlib, "2.7.0", "3ef16e77562f9855a2605900cedb15c1462d76fb1be6a32fc3ae91973ee543d2", [:rebar3], [], "hexpm"},
|
||||||
|
@ -9,6 +11,7 @@
|
||||||
"decimal": {:hex, :decimal, "1.6.0", "bfd84d90ff966e1f5d4370bdd3943432d8f65f07d3bab48001aebd7030590dcc", [:mix], [], "hexpm"},
|
"decimal": {:hex, :decimal, "1.6.0", "bfd84d90ff966e1f5d4370bdd3943432d8f65f07d3bab48001aebd7030590dcc", [:mix], [], "hexpm"},
|
||||||
"ecto": {:hex, :ecto, "3.0.3", "018a3df0956636f84eb3033d807485a7d3dea8474f47b90da5cb8073444c4384", [:mix], [{:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
|
"ecto": {:hex, :ecto, "3.0.3", "018a3df0956636f84eb3033d807485a7d3dea8474f47b90da5cb8073444c4384", [:mix], [{:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
|
||||||
"ecto_sql": {:hex, :ecto_sql, "3.0.2", "0e04cbc183b91ea0085c502226befcd237a4ac31c204fd4be8d4db6676b5f10d", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.2.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
|
"ecto_sql": {:hex, :ecto_sql, "3.0.2", "0e04cbc183b91ea0085c502226befcd237a4ac31c204fd4be8d4db6676b5f10d", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.2.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
|
"elixir_make": {:hex, :elixir_make, "0.5.2", "96a28c79f5b8d34879cd95ebc04d2a0d678cfbbd3e74c43cb63a76adf0ee8054", [:mix], [], "hexpm"},
|
||||||
"fiet": {:git, "https://github.com/shadowfacts/fiet.git", "bf117bc30a6355a189d05a562127cfaf9e0187ae", [branch: "master"]},
|
"fiet": {:git, "https://github.com/shadowfacts/fiet.git", "bf117bc30a6355a189d05a562127cfaf9e0187ae", [branch: "master"]},
|
||||||
"file_system": {:hex, :file_system, "0.2.6", "fd4dc3af89b9ab1dc8ccbcc214a0e60c41f34be251d9307920748a14bf41f1d3", [:mix], [], "hexpm"},
|
"file_system": {:hex, :file_system, "0.2.6", "fd4dc3af89b9ab1dc8ccbcc214a0e60c41f34be251d9307920748a14bf41f1d3", [:mix], [], "hexpm"},
|
||||||
"floki": {:hex, :floki, "0.20.4", "be42ac911fece24b4c72f3b5846774b6e61b83fe685c2fc9d62093277fb3bc86", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}, {:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
|
"floki": {:hex, :floki, "0.20.4", "be42ac911fece24b4c72f3b5846774b6e61b83fe685c2fc9d62093277fb3bc86", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}, {:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
defmodule Frenzy.Repo.Migrations.CreateUsers do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
create table(:users) do
|
||||||
|
add :username, :string
|
||||||
|
add :password_hash, :string
|
||||||
|
|
||||||
|
timestamps()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,9 @@
|
||||||
|
defmodule Frenzy.Repo.Migrations.GroupsAddUser do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:groups) do
|
||||||
|
add :user_id, references(:users, on_delete: :delete_all)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -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
|
Loading…
Reference in New Issue