diff --git a/lib/hostas/token.ex b/lib/hostas/token.ex index 307f558..0be40dd 100644 --- a/lib/hostas/token.ex +++ b/lib/hostas/token.ex @@ -1,6 +1,7 @@ defmodule Hostas.Token do use Ecto.Schema import Ecto.Changeset + import Ecto.Query, only: [from: 2] alias Hostas.Token alias Hostas.Repo @@ -11,12 +12,11 @@ defmodule Hostas.Token do @duration_days 30 schema "tokens" do - field :denizen_id, :integer field :expires, :utc_datetime field :token, :string # Link tokens to denizens - belongs_to :denizens, Hostas.Denizen + belongs_to :denizen, Hostas.Denizen timestamps() end @@ -24,8 +24,8 @@ defmodule Hostas.Token do @doc false def changeset(token, attrs) do token - |> cast(attrs, [:denizen_id, :expires]) - |> validate_required([:denizen_id, :expires]) + |> cast(attrs, [:denizen_id, :expires, :token]) + |> validate_required([:denizen_id, :expires, :token]) end @doc """ @@ -46,4 +46,41 @@ defmodule Hostas.Token do # Register the token Repo.insert(changeset(%Token{}, %{denizen_id: denizen_id, token: token, expires: expiry})) end + + @doc """ + Returns {:ok, struct} if the token hasn't expired. Otherwise, returns :expired + """ + def get(key) do + struct = Repo.one!(from t in Token, where: t.token == ^key) + + unless expired?(struct) do + {:ok, struct} + else + :expired + end + end + + @doc """ + Returns true if the given token has expired + """ + def expired?(%Token{expires: expiry}) do + {:ok, now} = DateTime.now("Etc/UTC") + case DateTime.compare(expiry, now) do + :lt -> true + _ -> false + end + end + + def expired?(token) when is_binary(token) do + expired?(Repo.one(from t in Token, where: t.token == ^token)) + end + + @doc """ + Deletes a token. Does nothing if the token doesn't exist + """ + def revoke(%Token{token: token}), do: revoke(token) + def revoke(token) when is_binary(token) do + Repo.delete_all(from t in Token, where: t.token == ^token) + :ok + end end diff --git a/test/hostas/token_test.exs b/test/hostas/token_test.exs new file mode 100644 index 0000000..bbec192 --- /dev/null +++ b/test/hostas/token_test.exs @@ -0,0 +1,57 @@ +defmodule Hostas.TokenTest do + use Hostas.DataCase + + # For testing with Ecto + import Ecto.Query, only: [from: 2] + alias Hostas.Repo + alias Hostas.Token + + describe "token generation" do + test "succeeds" do + {:ok, struct} = Token.new(create_denizen().id) + assert Repo.one!(from t in Token, where: t.id == ^struct.id) + end + end + + describe "check token expiry" do + test "by struct" do + {:ok, struct} = Token.new(create_denizen().id, -30) + assert Token.expired?(struct) + end + + test "by key" do + {:ok, struct} = Token.new(create_denizen().id, -30) + assert Token.expired?(struct.token) + end + end + + describe "fetch guarded against expiry" do + test "succeeds" do + {:ok, struct} = Token.new(create_denizen().id) + assert Token.get(struct.token) == {:ok, struct} + end + + test "indicates the token is expired" do + {:ok, struct} = Token.new(create_denizen().id, -30) + assert Token.get(struct.token) == :expired + end + + test "by key" do + {:ok, struct} = Token.new(create_denizen().id, -30) + assert Token.expired?(struct.token) + end + end + + describe "revoke token" do + test "succeeds, given struct" do + {:ok, struct} = Token.new(create_denizen().id) + assert Token.revoke(struct) == :ok + assert Repo.one(from t in Token, where: t.token == ^struct.token) == nil + end + + test "succeeds, given key" do + {:ok, struct} = Token.new(create_denizen().id) + assert Token.revoke(struct.token) == :ok + end + end +end