Skip to content

Commit

Permalink
Merge pull request #588 from podlove/feature/polymorphic-services
Browse files Browse the repository at this point in the history
 add userservices model
  • Loading branch information
electronicbites authored Nov 19, 2024
2 parents 82c131e + 1c6a4e5 commit 8760e4c
Show file tree
Hide file tree
Showing 14 changed files with 478 additions and 171 deletions.
68 changes: 60 additions & 8 deletions lib/radiator/accounts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule Radiator.Accounts do
import Ecto.Query, warn: false
alias Radiator.Repo

alias Radiator.Accounts.{User, UserNotifier, UserToken}
alias Radiator.Accounts.{User, UserNotifier, UserToken, WebService}

## Database getters

Expand Down Expand Up @@ -247,6 +247,27 @@ defmodule Radiator.Accounts do
end
end

@doc """
Get the user's Raindrop tokens if they exist.
## Examples
iex> get_raindrop_tokens(23)
%WebService{}
iex> get_raindrop_tokens(42)
nil
"""
def get_raindrop_tokens(user_id) do
service_name = WebService.raindrop_service_name()

WebService
|> where([w], w.user_id == ^user_id)
|> where([w], w.service_name == ^service_name)
|> Repo.one()
end

@doc """
Sets a users optional Raindrop tokens and expiration time.
Given a user id, access token, refresh token, and expiration time,
Expand All @@ -266,16 +287,47 @@ defmodule Radiator.Accounts do
raindrop_refresh_token,
raindrop_expires_at
) do
User
|> Radiator.Repo.get!(user_id)
|> User.set_raindrop_token_changeset(
raindrop_access_token,
raindrop_refresh_token,
raindrop_expires_at
%WebService{}
|> WebService.changeset(%{
service_name: WebService.raindrop_service_name(),
user_id: user_id,
data: %{
access_token: raindrop_access_token,
refresh_token: raindrop_refresh_token,
expires_at: raindrop_expires_at
}
})
|> Repo.insert(
on_conflict: {:replace_all_except, [:id, :created_at]},
conflict_target: [:user_id, :service_name],
set: [updated_at: DateTime.utc_now()]
)
|> Repo.update()
end

@doc """
Radiator.Accounts.connect_show_with_raindrop(1, 23, 42)
"""
def connect_show_with_raindrop(user_id, show_id, collection_id) do
case get_raindrop_tokens(user_id) do
nil ->
{:error, "No Raindrop tokens found"}

%{data: data} = service ->
data =
Map.update!(data, :collection_mappings, fn mappings ->
Map.put(mappings, show_id_to_collection_id(show_id), collection_id)
end)
|> Map.from_struct()

service
|> WebService.changeset(%{data: data})
|> Repo.update()
end
end

defp show_id_to_collection_id(show_id) when is_integer(show_id), do: Integer.to_string(show_id)
defp show_id_to_collection_id(show_id), do: show_id

## Session

@doc """
Expand Down
149 changes: 149 additions & 0 deletions lib/radiator/accounts/raindrop_client.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
defmodule Radiator.Accounts.RaindropClient do
@moduledoc """
Client for Raindrop API
"""
require Logger

alias Radiator.Accounts

def config, do: Application.fetch_env!(:radiator, :raindrop)

def redirect_uri_encoded(user_id) do
user_id
|> redirect_uri()
|> URI.encode()
end

def redirect_uri(user_id) do
config()[:redirect_url]
|> URI.parse()
|> URI.append_query("user_id=#{user_id}")
|> URI.to_string()
end

def redirect_uri do
config()[:redirect_url]
|> URI.parse()
|> URI.to_string()
end

@doc """
Check if the user has access to Raindrop API
"""
def access_enabled?(user_id) do
not_enabled =
user_id
|> Accounts.get_raindrop_tokens()
|> is_nil()

!not_enabled
end

@doc """
Get all collections for a user
"""
def get_collections(user_id) do
service =
user_id
|> Accounts.get_raindrop_tokens()
|> refresh_token_if()

if is_nil(service) do
{:error, :unauthorized}
else
[
method: :get,
url: "https://api.raindrop.io/rest/v1/collections",
headers: [
{"Authorization", "Bearer #{service.data.access_token}"}
]
]
|> Req.request()
|> parse_collection_response()
end
end

@doc """
first time fetching access token and storing it as webservice entry
"""
def init_and_store_access_token(user_id, code) do
{:ok, response} =
[
method: :post,
url: "https://raindrop.io/oauth/access_token",
json: %{
client_id: config()[:client_id],
client_secret: config()[:client_secret],
grant_type: "authorization_code",
code: code,
redirect_uri: redirect_uri()
}
]
|> Keyword.merge(config()[:options])
|> Req.request()

parse_access_token_response(response, user_id)
end

defp refresh_token_if(service) do
if DateTime.before?(service.data.expires_at, DateTime.utc_now()) do
{:ok, response} =
[
method: :post,
url: "https://raindrop.io/oauth/access_token",
headers: [
{"Content-Type", "application/json"}
],
json: %{
client_id: config()[:client_id],
client_secret: config()[:client_secret],
grant_type: "refresh_token",
refresh_token: service.data.refresh_token
}
]
|> Req.request()

parse_access_token_response(response, service.user_id)
else
service
end
end

defp parse_access_token_response(
%Req.Response{
body: %{
"access_token" => access_token,
"refresh_token" => refresh_token,
"expires_in" => expires_in
}
},
user_id
) do
expires_at =
DateTime.now!("Etc/UTC")
|> DateTime.shift(second: expires_in)
|> DateTime.truncate(:second)

Accounts.update_raindrop_tokens(
user_id,
access_token,
refresh_token,
expires_at
)
end

defp parse_access_token_response(response, _user_id) do
Logger.error("Error fetching access token: #{inspect(response)}")
{:error, :unauthorized}
end

defp parse_collection_response({:ok, %Req.Response{status: 401}}) do
{:error, :unauthorized}
end

defp parse_collection_response({:ok, %Req.Response{body: body}}) do
body
|> Map.get("items")
|> Enum.map(&Map.take(&1, ["_id", "title"]))
end
end
16 changes: 2 additions & 14 deletions lib/radiator/accounts/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ defmodule Radiator.Accounts.User do
"""
use Ecto.Schema
import Ecto.Changeset
alias Radiator.Accounts.WebService
alias Radiator.Podcast.Show

schema "users" do
Expand All @@ -12,10 +13,8 @@ defmodule Radiator.Accounts.User do
field :hashed_password, :string, redact: true
field :current_password, :string, virtual: true, redact: true
field :confirmed_at, :utc_datetime
field :raindrop_access_token, :string, redact: true
field :raindrop_refresh_token, :string, redact: true
field :raindrop_expires_at, :utc_datetime

has_many :services, WebService
many_to_many :hosting_shows, Show, join_through: "show_hosts"

timestamps(type: :utc_datetime)
Expand Down Expand Up @@ -137,17 +136,6 @@ defmodule Radiator.Accounts.User do
change(user, confirmed_at: now)
end

@doc """
Sets the raindrop tokens and expiration time.
"""
def set_raindrop_token_changeset(user, access_token, refresh_token, expires_at) do
change(user,
raindrop_access_token: access_token,
raindrop_refresh_token: refresh_token,
raindrop_expires_at: expires_at
)
end

@doc """
Verifies the password.
Expand Down
45 changes: 45 additions & 0 deletions lib/radiator/accounts/web_service.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
defmodule Radiator.Accounts.WebService do
@moduledoc """
Model for storing all kinds of information about a user's service.
First implementation is for Raindrop.io
In the future we may have support for other services and https://hexdocs.pm/polymorphic_embed/ might be a solution
"""
use Ecto.Schema
import Ecto.Changeset

alias Radiator.Accounts.User

@raindrop_service_name "raindrop"

schema "web_services" do
field :service_name, :string

embeds_one :data, RaindropService, on_replace: :delete, primary_key: false do
field :access_token, :string, redact: true
field :refresh_token, :string, redact: true
field :expires_at, :utc_datetime
# Show ID => Raindrop Collection ID
field :collection_mappings, :map, default: %{}
end

belongs_to :user, User

timestamps(type: :utc_datetime)
end

@doc false
def changeset(service, attrs) do
service
|> cast(attrs, [:service_name, :user_id])
|> cast_embed(:data, required: true, with: &raindrop_changeset/2)
|> validate_required([:service_name, :data])
end

def raindrop_changeset(service, attrs \\ %{}) do
service
|> cast(attrs, [:access_token, :refresh_token, :expires_at, :collection_mappings])
|> validate_required([:access_token, :refresh_token, :expires_at])
end

def raindrop_service_name, do: @raindrop_service_name
end
67 changes: 0 additions & 67 deletions lib/radiator/raindrop_client.ex

This file was deleted.

Loading

0 comments on commit 8760e4c

Please sign in to comment.