mirror of
https://github.com/ClaperCo/Claper.git
synced 2025-12-16 11:57:58 +01:00
Fix tests and credo
This commit is contained in:
@@ -97,7 +97,7 @@ defmodule Claper.Events do
|
||||
event = Repo.get_by!(Event, uuid: id)
|
||||
|
||||
is_leader =
|
||||
Claper.Events.is_leaded_by(current_user.email, event) || event.user_id == current_user.id
|
||||
Claper.Events.leaded_by?(current_user.email, event) || event.user_id == current_user.id
|
||||
|
||||
if is_leader do
|
||||
event |> Repo.preload(preload)
|
||||
@@ -177,12 +177,12 @@ defmodule Claper.Events do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> is_leaded_by("email@example.com", 123)
|
||||
iex> leaded_by?("email@example.com", 123)
|
||||
true
|
||||
|
||||
|
||||
"""
|
||||
def is_leaded_by(email, event) do
|
||||
def leaded_by?(email, event) do
|
||||
from(a in ActivityLeader,
|
||||
join: u in Claper.Accounts.User,
|
||||
on: u.email == a.email,
|
||||
|
||||
@@ -7,50 +7,64 @@ defmodule ClaperWeb.Lti.GradeController do
|
||||
def create(conn, _params) do
|
||||
resource_id = conn |> get_session(:resource_id) |> String.to_integer()
|
||||
user_id = conn |> get_session(:user_id)
|
||||
timestamp = get_timestamp()
|
||||
|
||||
{:ok, dt} = DateTime.now("Etc/UTC")
|
||||
timestamp = DateTime.to_iso8601(dt)
|
||||
|
||||
case Lti13.Tool.Services.AccessToken.fetch_access_token(
|
||||
%{
|
||||
auth_token_url: "http://localhost.charlesproxy.com/mod/lti/token.php",
|
||||
client_id: "NQQ8egz8Kj1s1qw",
|
||||
auth_server: "http://localhost.charlesproxy.com"
|
||||
},
|
||||
[
|
||||
"https://purl.imsglobal.org/spec/lti-ags/scope/lineitem",
|
||||
"https://purl.imsglobal.org/spec/lti-ags/scope/score"
|
||||
],
|
||||
"http://localhost:4000"
|
||||
) do
|
||||
case fetch_access_token() do
|
||||
{:ok, access_token} ->
|
||||
case AGS.fetch_or_create_line_item(
|
||||
"http://localhost.charlesproxy.com/mod/lti/services.php/2/lineitems?type_id=2",
|
||||
resource_id,
|
||||
fn -> 100.0 end,
|
||||
"test",
|
||||
access_token
|
||||
) do
|
||||
{:ok, line_item} ->
|
||||
AGS.post_score(
|
||||
%Score{
|
||||
scoreGiven: 90.0,
|
||||
scoreMaximum: 100.0,
|
||||
activityProgress: "Completed",
|
||||
gradingProgress: "FullyGraded",
|
||||
userId: user_id,
|
||||
comment: "",
|
||||
timestamp: timestamp
|
||||
},
|
||||
line_item,
|
||||
access_token
|
||||
)
|
||||
|
||||
conn |> send_resp(200, "")
|
||||
end
|
||||
handle_line_item(conn, resource_id, user_id, timestamp, access_token)
|
||||
|
||||
{:error, msg} ->
|
||||
conn |> send_resp(500, msg)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_timestamp do
|
||||
{:ok, dt} = DateTime.now("Etc/UTC")
|
||||
DateTime.to_iso8601(dt)
|
||||
end
|
||||
|
||||
defp fetch_access_token do
|
||||
Lti13.Tool.Services.AccessToken.fetch_access_token(
|
||||
%{
|
||||
auth_token_url: "http://localhost.charlesproxy.com/mod/lti/token.php",
|
||||
client_id: "NQQ8egz8Kj1s1qw",
|
||||
auth_server: "http://localhost.charlesproxy.com"
|
||||
},
|
||||
[
|
||||
"https://purl.imsglobal.org/spec/lti-ags/scope/lineitem",
|
||||
"https://purl.imsglobal.org/spec/lti-ags/scope/score"
|
||||
],
|
||||
"http://localhost:4000"
|
||||
)
|
||||
end
|
||||
|
||||
defp handle_line_item(conn, resource_id, user_id, timestamp, access_token) do
|
||||
case AGS.fetch_or_create_line_item(
|
||||
"http://localhost.charlesproxy.com/mod/lti/services.php/2/lineitems?type_id=2",
|
||||
resource_id,
|
||||
fn -> 100.0 end,
|
||||
"test",
|
||||
access_token
|
||||
) do
|
||||
{:ok, line_item} ->
|
||||
post_score(line_item, user_id, timestamp, access_token)
|
||||
conn |> send_resp(200, "")
|
||||
end
|
||||
end
|
||||
|
||||
defp post_score(line_item, user_id, timestamp, access_token) do
|
||||
AGS.post_score(
|
||||
%Score{
|
||||
scoreGiven: 90.0,
|
||||
scoreMaximum: 100.0,
|
||||
activityProgress: "Completed",
|
||||
gradingProgress: "FullyGraded",
|
||||
userId: user_id,
|
||||
comment: "",
|
||||
timestamp: timestamp
|
||||
},
|
||||
line_item,
|
||||
access_token
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -178,8 +178,6 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
|
||||
<div>
|
||||
<%= if not @is_leader do %>
|
||||
<a
|
||||
data-phx-link="patch"
|
||||
data-phx-link-state="push"
|
||||
href={~p"/events/#{@event.uuid}/edit"}
|
||||
class="flex w-full lg:w-auto rounded-md tracking-wide focus:outline-none focus:shadow-outline text-primary-500 text-sm items-center"
|
||||
>
|
||||
@@ -199,8 +197,6 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
|
||||
<div>
|
||||
<%= if not @is_leader do %>
|
||||
<a
|
||||
data-phx-link="patch"
|
||||
data-phx-link-state="push"
|
||||
href={~p"/events/#{@event.uuid}/edit"}
|
||||
class="flex w-full lg:w-auto rounded-md tracking-wide focus:outline-none focus:shadow-outline text-primary-500 text-sm items-center"
|
||||
>
|
||||
|
||||
@@ -18,7 +18,7 @@ defmodule ClaperWeb.EventLive.Manage do
|
||||
presentation_file: [:polls, :presentation_state]
|
||||
])
|
||||
|
||||
if is_nil(event) || not is_leader(socket, event) do
|
||||
if is_nil(event) || not leader?(socket, event) do
|
||||
{:ok,
|
||||
socket
|
||||
|> put_flash(:error, gettext("Event doesn't exist"))
|
||||
@@ -77,11 +77,11 @@ defmodule ClaperWeb.EventLive.Manage do
|
||||
end
|
||||
end
|
||||
|
||||
defp is_leader(%{assigns: %{current_user: current_user}} = _socket, event) do
|
||||
Claper.Events.is_leaded_by(current_user.email, event) || event.user.id == current_user.id
|
||||
defp leader?(%{assigns: %{current_user: current_user}} = _socket, event) do
|
||||
Claper.Events.leaded_by?(current_user.email, event) || event.user.id == current_user.id
|
||||
end
|
||||
|
||||
defp is_leader(_socket, _event), do: false
|
||||
defp leader?(_socket, _event), do: false
|
||||
|
||||
@impl true
|
||||
def handle_info(%{event: "presence_diff"}, %{assigns: %{event: event}} = socket) do
|
||||
|
||||
@@ -22,19 +22,19 @@ defmodule ClaperWeb.EventLive.PostComponent do
|
||||
<img src="/images/icons/ellipsis-horizontal-white.svg" class="h-5" />
|
||||
</button>
|
||||
|
||||
<%= if @post.name || is_a_leader(@post, @event, @leaders) || is_pinned(@post) do %>
|
||||
<%= if @post.name || leader?(@post, @event, @leaders) || pinned?(@post) do %>
|
||||
<div class="inline-flex items-center">
|
||||
<%= if @post.name do %>
|
||||
<p class="text-white text-xs font-semibold mb-2 mr-2"><%= @post.name %></p>
|
||||
<% end %>
|
||||
<%= if is_a_leader(@post, @event, @leaders) do %>
|
||||
<%= if leader?(@post, @event, @leaders) do %>
|
||||
<div class="inline-flex items-center space-x-1 justify-center px-3 py-0.5 rounded-full text-xs font-medium bg-supporting-yellow-100 text-supporting-yellow-800 mb-2">
|
||||
<img src="/images/icons/star.svg" class="h-3" />
|
||||
<span><%= gettext("Host") %></span>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= if is_pinned(@post) do %>
|
||||
<%= if pinned?(@post) do %>
|
||||
<div class="inline-flex items-center space-x-1 justify-center px-3 py-0.5 rounded-full text-xs font-medium bg-supporting-yellow-100 text-supporting-yellow-800 mb-2 ml-1">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -101,12 +101,12 @@ defmodule ClaperWeb.EventLive.PostComponent do
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="px-4 pt-3 pb-8 rounded-b-lg rounded-tr-lg bg-white text-black relative z-0 break-all">
|
||||
<%= if @post.name || is_a_leader(@post, @event, @leaders) do %>
|
||||
<%= if @post.name || leader?(@post, @event, @leaders) do %>
|
||||
<div class="inline-flex items-center">
|
||||
<%= if @post.name do %>
|
||||
<p class="text-black text-xs font-semibold mb-2 mr-2"><%= @post.name %></p>
|
||||
<% end %>
|
||||
<%= if is_a_leader(@post, @event, @leaders) do %>
|
||||
<%= if leader?(@post, @event, @leaders) do %>
|
||||
<div class="inline-flex items-center space-x-1 justify-center px-3 py-0.5 rounded-full text-xs font-medium bg-supporting-yellow-100 text-supporting-yellow-800 mb-2">
|
||||
<img src="/images/icons/star.svg" class="h-3" />
|
||||
<span><%= gettext("Host") %></span>
|
||||
@@ -150,7 +150,7 @@ defmodule ClaperWeb.EventLive.PostComponent do
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= if is_pinned(@post) do %>
|
||||
<%= if pinned?(@post) do %>
|
||||
<div class="inline-flex items-center space-x-1 justify-center px-3 py-0.5 rounded-full text-xs font-medium bg-supporting-yellow-100 text-supporting-yellow-800 mb-2 ml-1">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -265,7 +265,7 @@ defmodule ClaperWeb.EventLive.PostComponent do
|
||||
"""
|
||||
end
|
||||
|
||||
defp is_a_leader(post, event, leaders) do
|
||||
defp leader?(post, event, leaders) do
|
||||
!is_nil(post.user_id) &&
|
||||
(post.user_id == event.user_id ||
|
||||
Enum.any?(leaders, fn leader ->
|
||||
@@ -273,7 +273,7 @@ defmodule ClaperWeb.EventLive.PostComponent do
|
||||
end))
|
||||
end
|
||||
|
||||
defp is_pinned(post) do
|
||||
defp pinned?(post) do
|
||||
post.pinned == true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,7 +18,7 @@ defmodule ClaperWeb.EventLive.Presenter do
|
||||
presentation_file: [:polls, :presentation_state]
|
||||
])
|
||||
|
||||
if is_nil(event) || not is_leader(socket, event) do
|
||||
if is_nil(event) || not leader?(socket, event) do
|
||||
{:ok,
|
||||
socket
|
||||
|> put_flash(:error, gettext("Event doesn't exist"))
|
||||
@@ -75,11 +75,11 @@ defmodule ClaperWeb.EventLive.Presenter do
|
||||
end)
|
||||
end
|
||||
|
||||
defp is_leader(%{assigns: %{current_user: current_user}} = _socket, event) do
|
||||
Claper.Events.is_leaded_by(current_user.email, event) || event.user.id == current_user.id
|
||||
defp leader?(%{assigns: %{current_user: current_user}} = _socket, event) do
|
||||
Claper.Events.leaded_by?(current_user.email, event) || event.user.id == current_user.id
|
||||
end
|
||||
|
||||
defp is_leader(_socket, _event), do: false
|
||||
defp leader?(_socket, _event), do: false
|
||||
|
||||
@impl true
|
||||
def handle_info(%{event: "presence_diff"}, %{assigns: %{event: event}} = socket) do
|
||||
|
||||
@@ -108,7 +108,7 @@ defmodule ClaperWeb.EventLive.Show do
|
||||
defp check_leader(%{assigns: %{current_user: current_user} = _assigns} = socket, event)
|
||||
when is_map(current_user) do
|
||||
is_leader =
|
||||
current_user.id == event.user_id || Claper.Events.is_leaded_by(current_user.email, event)
|
||||
current_user.id == event.user_id || Claper.Events.leaded_by?(current_user.email, event)
|
||||
|
||||
socket |> assign(:is_leader, is_leader)
|
||||
end
|
||||
|
||||
@@ -6,8 +6,7 @@ defmodule Lti13.Jwks.Utils.KeyGenerator do
|
||||
Create a random passphrase of size given (defaults to 256)
|
||||
"""
|
||||
def passphrase(len \\ 256) do
|
||||
Enum.map(1..len, fn _i -> Enum.random(@chars) end)
|
||||
|> Enum.join("")
|
||||
Enum.map_join(1..len, "", fn _i -> Enum.random(@chars) end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -21,7 +20,7 @@ defmodule Lti13.Jwks.Utils.KeyGenerator do
|
||||
def generate_key_pair do
|
||||
key_id = passphrase()
|
||||
|
||||
{:ok, rsa_priv_key} = generate_key(:rsa, 4096, 65537)
|
||||
{:ok, rsa_priv_key} = generate_key(:rsa, 4096, 65_537)
|
||||
{:ok, public_key} = public_key_from_private_key(rsa_priv_key)
|
||||
{:ok, private_key_pem} = pem_entry_encode(rsa_priv_key, :RSAPrivateKey)
|
||||
{:ok, public_key_pem} = pem_entry_encode(public_key, :RSAPublicKey)
|
||||
|
||||
@@ -93,21 +93,21 @@ defmodule Lti13.Jwks.Validator do
|
||||
end
|
||||
end
|
||||
|
||||
def validate_user(%{
|
||||
"sub" => sub,
|
||||
"name" => name,
|
||||
"email" => email,
|
||||
"iss" => issuer,
|
||||
"aud" => client_id,
|
||||
"https://purl.imsglobal.org/spec/lti/claim/roles" => roles
|
||||
}) do
|
||||
def validate_user(
|
||||
%{
|
||||
"sub" => sub,
|
||||
"name" => name,
|
||||
"email" => email,
|
||||
"https://purl.imsglobal.org/spec/lti/claim/roles" => roles
|
||||
},
|
||||
registration
|
||||
) do
|
||||
case Lti13.Users.get_or_create_user(%{
|
||||
sub: sub,
|
||||
name: name,
|
||||
email: email,
|
||||
roles: roles,
|
||||
issuer: issuer,
|
||||
client_id: client_id
|
||||
registration_id: registration.id
|
||||
}) do
|
||||
{:error, _} ->
|
||||
{:error, %{reason: :invalid_user, msg: "Invalid user"}}
|
||||
@@ -146,31 +146,37 @@ defmodule Lti13.Jwks.Validator do
|
||||
def fetch_public_key(key_set_url, kid) do
|
||||
public_key_set =
|
||||
case Req.get(key_set_url) do
|
||||
{:ok, %Req.Response{status: 200, body: body}} ->
|
||||
body
|
||||
|
||||
error ->
|
||||
error
|
||||
{:ok, %Req.Response{status: 200, body: body}} -> body
|
||||
error -> error
|
||||
end
|
||||
|
||||
find_and_process_key(public_key_set, kid)
|
||||
end
|
||||
|
||||
defp find_and_process_key(public_key_set, kid) do
|
||||
if container?(public_key_set) do
|
||||
case Enum.find(public_key_set["keys"], fn key -> container?(key) && key["kid"] == kid end) do
|
||||
nil ->
|
||||
return_key_not_found(kid)
|
||||
|
||||
public_key_json ->
|
||||
public_key =
|
||||
public_key_json
|
||||
|> convert_map_to_base64url()
|
||||
|> JOSE.JWK.from()
|
||||
|
||||
{:ok, public_key}
|
||||
end
|
||||
find_key_in_set(public_key_set, kid)
|
||||
else
|
||||
return_key_not_found(kid)
|
||||
end
|
||||
end
|
||||
|
||||
defp find_key_in_set(public_key_set, kid) do
|
||||
case Enum.find(public_key_set["keys"], fn key -> container?(key) && key["kid"] == kid end) do
|
||||
nil -> return_key_not_found(kid)
|
||||
public_key_json -> process_public_key(public_key_json)
|
||||
end
|
||||
end
|
||||
|
||||
defp process_public_key(public_key_json) do
|
||||
public_key =
|
||||
public_key_json
|
||||
|> convert_map_to_base64url()
|
||||
|> JOSE.JWK.from()
|
||||
|
||||
{:ok, public_key}
|
||||
end
|
||||
|
||||
defp container?(container) do
|
||||
Keyword.keyword?(container) || is_map(container) || is_struct(container)
|
||||
end
|
||||
|
||||
@@ -20,7 +20,7 @@ defmodule Lti13.Nonces do
|
||||
end
|
||||
|
||||
# 86400 seconds = 24 hours
|
||||
def delete_expired_nonces(nonce_ttl_sec \\ 86_4000) do
|
||||
def delete_expired_nonces(nonce_ttl_sec \\ 86_400) do
|
||||
nonce_expiry = DateTime.utc_now() |> DateTime.add(-nonce_ttl_sec, :second)
|
||||
Repo.delete_all(from(n in Nonce, where: n.inserted_at < ^nonce_expiry))
|
||||
end
|
||||
|
||||
@@ -14,7 +14,10 @@ defmodule Lti13.Resources do
|
||||
where: r.resource_id == ^resource_id and r.registration_id == ^registration_id
|
||||
)
|
||||
|> Repo.one()
|
||||
|> Repo.preload(:event)
|
||||
|> case do
|
||||
nil -> nil
|
||||
resource -> resource |> Repo.preload(:event)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -25,7 +25,7 @@ defmodule Lti13.Tool.LaunchValidation do
|
||||
{:ok} <- validate_timestamps(jwt_body),
|
||||
{:ok} <- validate_deployment(registration, jwt_body),
|
||||
{:ok} <- validate_message(jwt_body),
|
||||
{:ok, lti_user} <- validate_user(jwt_body),
|
||||
{:ok, lti_user} <- validate_user(jwt_body, registration),
|
||||
{:ok} <- validate_nonce(lti_user, jwt_body, "validate_launch"),
|
||||
{:ok, is_instructor} <- validate_role(jwt_body),
|
||||
{:ok, resource} <- validate_resource(jwt_body, lti_user, registration, is_instructor),
|
||||
@@ -46,25 +46,22 @@ defmodule Lti13.Tool.LaunchValidation do
|
||||
"State from session is missing. Make sure cookies are enabled and configured correctly"
|
||||
}}
|
||||
|
||||
session_state ->
|
||||
case params["state"] do
|
||||
nil ->
|
||||
{:error, %{reason: :invalid_oidc_state, msg: "State from OIDC request is missing"}}
|
||||
|
||||
request_state ->
|
||||
if request_state == session_state do
|
||||
{:ok}
|
||||
else
|
||||
{:error,
|
||||
%{
|
||||
reason: :invalid_oidc_state,
|
||||
msg: "State from OIDC request does not match session"
|
||||
}}
|
||||
end
|
||||
end
|
||||
_ ->
|
||||
compare_oidc_states(params["state"], session_state)
|
||||
end
|
||||
end
|
||||
|
||||
defp compare_oidc_states(nil, _),
|
||||
do: {:error, %{reason: :invalid_oidc_state, msg: "State from OIDC request is missing"}}
|
||||
|
||||
defp compare_oidc_states(request_state, session_state) when request_state == session_state,
|
||||
do: {:ok}
|
||||
|
||||
defp compare_oidc_states(_, _),
|
||||
do:
|
||||
{:error,
|
||||
%{reason: :invalid_oidc_state, msg: "State from OIDC request does not match session"}}
|
||||
|
||||
defp validate_registration(params) do
|
||||
with {:ok, issuer, client_id} <- peek_issuer_client_id(params) do
|
||||
case Registrations.get_registration_by_issuer_client_id(issuer, client_id) do
|
||||
@@ -102,47 +99,42 @@ defmodule Lti13.Tool.LaunchValidation do
|
||||
is_instructor
|
||||
) do
|
||||
case Lti13.Resources.get_resource_by_id_and_registration(resource_id, registration.id) do
|
||||
nil ->
|
||||
case is_instructor do
|
||||
true ->
|
||||
case Lti13.Resources.create_resource_with_event(%{
|
||||
title: title,
|
||||
resource_id: resource_id,
|
||||
lti_user: lti_user
|
||||
}) do
|
||||
{:ok, resource} ->
|
||||
{:ok, resource}
|
||||
nil -> handle_missing_resource(title, resource_id, lti_user, is_instructor)
|
||||
resource -> handle_existing_resource(resource, lti_user, is_instructor)
|
||||
end
|
||||
end
|
||||
|
||||
{:error, _} ->
|
||||
{:error, %{reason: :invalid_resource, msg: "Failed to create resource"}}
|
||||
end
|
||||
defp handle_missing_resource(title, resource_id, lti_user, true) do
|
||||
case Lti13.Resources.create_resource_with_event(%{
|
||||
title: title,
|
||||
resource_id: resource_id,
|
||||
lti_user: lti_user
|
||||
}) do
|
||||
{:ok, resource} -> {:ok, resource}
|
||||
{:error, _} -> {:error, %{reason: :invalid_resource, msg: "Failed to create resource"}}
|
||||
end
|
||||
end
|
||||
|
||||
false ->
|
||||
{:error,
|
||||
%{reason: :invalid_resource, msg: "User is not authorized to create resource"}}
|
||||
end
|
||||
defp handle_missing_resource(_, _, _, false),
|
||||
do: {:error, %{reason: :invalid_resource, msg: "User is not authorized to create resource"}}
|
||||
|
||||
resource ->
|
||||
case is_instructor do
|
||||
true ->
|
||||
with activity_leaders <-
|
||||
Claper.Events.get_activity_leaders_for_event(resource.event_id),
|
||||
activity_leaders_emails <- Enum.map(activity_leaders, fn al -> al.email end) do
|
||||
if lti_user.email not in activity_leaders_emails &&
|
||||
resource.event.user_id != lti_user.user_id do
|
||||
Claper.Events.create_activity_leader(%{
|
||||
email: lti_user.email,
|
||||
user_id: lti_user.id,
|
||||
event_id: resource.event_id
|
||||
})
|
||||
end
|
||||
end
|
||||
defp handle_existing_resource(resource, lti_user, true) do
|
||||
maybe_create_activity_leader(resource, lti_user)
|
||||
{:ok, resource}
|
||||
end
|
||||
|
||||
{:ok, resource}
|
||||
defp handle_existing_resource(resource, _, false), do: {:ok, resource}
|
||||
|
||||
false ->
|
||||
{:ok, resource}
|
||||
end
|
||||
defp maybe_create_activity_leader(resource, lti_user) do
|
||||
activity_leaders = Claper.Events.get_activity_leaders_for_event(resource.event_id)
|
||||
activity_leaders_emails = Enum.map(activity_leaders, fn al -> al.email end)
|
||||
|
||||
if lti_user.email not in activity_leaders_emails && resource.event.user_id != lti_user.user_id do
|
||||
Claper.Events.create_activity_leader(%{
|
||||
email: lti_user.email,
|
||||
user_id: lti_user.id,
|
||||
event_id: resource.event_id
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -187,32 +179,35 @@ defmodule Lti13.Tool.LaunchValidation do
|
||||
{:error, %{reason: :invalid_message_type, msg: "Missing message type"}}
|
||||
|
||||
message_type ->
|
||||
# no more than one message validator should apply for a given mesage,
|
||||
# so use the first validator we find that applies
|
||||
validation_result =
|
||||
case Enum.find(@message_validators, fn mv -> mv.can_validate(jwt_body) end) do
|
||||
nil -> nil
|
||||
validator -> validator.validate(jwt_body)
|
||||
end
|
||||
validate_message_type(jwt_body, message_type)
|
||||
end
|
||||
end
|
||||
|
||||
case validation_result do
|
||||
nil ->
|
||||
{:error,
|
||||
%{
|
||||
reason: :invalid_message_type,
|
||||
msg: "Invalid or unsupported message type \"#{message_type}\""
|
||||
}}
|
||||
defp validate_message_type(jwt_body, message_type) do
|
||||
case apply_message_validator(jwt_body) do
|
||||
nil ->
|
||||
{:error,
|
||||
%{
|
||||
reason: :invalid_message_type,
|
||||
msg: "Invalid or unsupported message type \"#{message_type}\""
|
||||
}}
|
||||
|
||||
{:error, error} ->
|
||||
{:error,
|
||||
%{
|
||||
reason: :invalid_message,
|
||||
msg: "Message validation failed: (\"#{message_type}\") #{error}"
|
||||
}}
|
||||
{:error, error} ->
|
||||
{:error,
|
||||
%{
|
||||
reason: :invalid_message,
|
||||
msg: "Message validation failed: (\"#{message_type}\") #{error}"
|
||||
}}
|
||||
|
||||
_ ->
|
||||
{:ok}
|
||||
end
|
||||
_ ->
|
||||
{:ok}
|
||||
end
|
||||
end
|
||||
|
||||
defp apply_message_validator(jwt_body) do
|
||||
case Enum.find(@message_validators, fn mv -> mv.can_validate(jwt_body) end) do
|
||||
nil -> nil
|
||||
validator -> validator.validate(jwt_body)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,37 +10,31 @@ defmodule Lti13.Users do
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
def get_user_by_sub(sub) do
|
||||
Repo.get_by(User, sub: sub)
|
||||
def get_user_by_sub_and_registration_id(sub, registration_id) do
|
||||
Repo.get_by(User, sub: sub, registration_id: registration_id)
|
||||
end
|
||||
|
||||
def get_or_create_user(%{sub: sub, email: email, issuer: issuer, client_id: client_id} = attrs) do
|
||||
case get_user_by_sub(sub) do
|
||||
nil ->
|
||||
case Claper.Accounts.get_user_by_email_or_create(email) do
|
||||
{:ok, claper_user} ->
|
||||
%{id: registration_id} =
|
||||
Lti13.Registrations.get_registration_by_issuer_client_id(issuer, client_id)
|
||||
def get_or_create_user(
|
||||
%{
|
||||
sub: sub,
|
||||
email: email,
|
||||
registration_id: registration_id
|
||||
} = attrs
|
||||
) do
|
||||
case get_user_by_sub_and_registration_id(sub, registration_id) do
|
||||
nil -> create_new_user(attrs, email, registration_id)
|
||||
%User{} = user -> {:ok, user |> Repo.preload(:user)}
|
||||
end
|
||||
end
|
||||
|
||||
updated_attrs =
|
||||
attrs
|
||||
|> Map.put(:user_id, claper_user.id)
|
||||
|> Map.put(:registration_id, registration_id)
|
||||
|
||||
case create_user(updated_attrs) do
|
||||
{:ok, user} ->
|
||||
{:ok, user |> Repo.preload(:user)}
|
||||
|
||||
{:error, _} ->
|
||||
{:error, %{reason: :invalid_user, msg: "Invalid user"}}
|
||||
end
|
||||
|
||||
{:error, _} ->
|
||||
{:error, %{reason: :invalid_user, msg: "Invalid Claper user"}}
|
||||
end
|
||||
|
||||
%User{} = user ->
|
||||
{:ok, user |> Repo.preload(:user)}
|
||||
defp create_new_user(attrs, email, registration_id) do
|
||||
with {:ok, claper_user} <- Claper.Accounts.get_user_by_email_or_create(email),
|
||||
updated_attrs <-
|
||||
Map.merge(attrs, %{user_id: claper_user.id, registration_id: registration_id}),
|
||||
{:ok, user} <- create_user(updated_attrs) do
|
||||
{:ok, user |> Repo.preload(:user)}
|
||||
else
|
||||
_ -> {:error, %{reason: :invalid_user, msg: "Invalid user"}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -39,7 +39,7 @@ defmodule Claper.Repo.Migrations.AddLtiTables do
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create unique_index(:lti_13_users, :sub)
|
||||
create unique_index(:lti_13_users, [:sub, :registration_id])
|
||||
|
||||
create table(:lti_13_nonces) do
|
||||
add :value, :string
|
||||
|
||||
@@ -14,21 +14,28 @@ defmodule Claper.EmbedsTest do
|
||||
presentation_file = presentation_file_fixture()
|
||||
embed = embed_fixture(%{presentation_file_id: presentation_file.id})
|
||||
|
||||
assert Embeds.list_embeds(presentation_file.id) == [embed]
|
||||
embeds = Embeds.list_embeds(presentation_file.id)
|
||||
assert [%Embed{} | _] = embeds
|
||||
assert length(embeds) == 1
|
||||
assert hd(embeds).id == embed.id
|
||||
end
|
||||
|
||||
test "list_embeds_at_position/2 returns all embeds from a presentation at a given position" do
|
||||
presentation_file = presentation_file_fixture()
|
||||
embed = embed_fixture(%{presentation_file_id: presentation_file.id, position: 5})
|
||||
|
||||
assert Embeds.list_embeds_at_position(presentation_file.id, 5) == [embed]
|
||||
embeds = Embeds.list_embeds_at_position(presentation_file.id, 5)
|
||||
assert [%Embed{} | _] = embeds
|
||||
assert length(embeds) == 1
|
||||
assert hd(embeds).id == embed.id
|
||||
assert hd(embeds).position == 5
|
||||
end
|
||||
|
||||
test "get_embed!/1 returns the embed with given id" do
|
||||
presentation_file = presentation_file_fixture()
|
||||
|
||||
embed = embed_fixture(%{presentation_file_id: presentation_file.id})
|
||||
assert Embeds.get_embed!(embed.id) == embed
|
||||
assert Embeds.get_embed!(embed.id, presentation_file: [:event]) == embed
|
||||
end
|
||||
|
||||
test "create_embed/1 with valid data creates a embed" do
|
||||
@@ -79,7 +86,7 @@ defmodule Claper.EmbedsTest do
|
||||
assert {:error, %Ecto.Changeset{}} =
|
||||
Embeds.update_embed(presentation_file.event_id, embed, @invalid_attrs)
|
||||
|
||||
assert embed == Embeds.get_embed!(embed.id)
|
||||
assert embed == Embeds.get_embed!(embed.id, presentation_file: [:event])
|
||||
end
|
||||
|
||||
test "delete_embed/2 deletes the embed" do
|
||||
|
||||
@@ -14,21 +14,33 @@ defmodule Claper.FormsTest do
|
||||
presentation_file = presentation_file_fixture()
|
||||
form = form_fixture(%{presentation_file_id: presentation_file.id})
|
||||
|
||||
assert Forms.list_forms(presentation_file.id) == [form]
|
||||
forms = Forms.list_forms(presentation_file.id)
|
||||
assert [%Form{} | _] = forms
|
||||
assert length(forms) == 1
|
||||
assert hd(forms).id == form.id
|
||||
end
|
||||
|
||||
test "list_forms_at_position/2 returns all forms from a presentation at a given position" do
|
||||
presentation_file = presentation_file_fixture()
|
||||
form = form_fixture(%{presentation_file_id: presentation_file.id, position: 5})
|
||||
|
||||
assert Forms.list_forms_at_position(presentation_file.id, 5) == [form]
|
||||
forms = Forms.list_forms_at_position(presentation_file.id, 5)
|
||||
assert [%Form{} | _] = forms
|
||||
assert length(forms) == 1
|
||||
assert hd(forms).id == form.id
|
||||
assert hd(forms).position == 5
|
||||
end
|
||||
|
||||
test "get_form!/1 returns the form with given id" do
|
||||
presentation_file = presentation_file_fixture()
|
||||
|
||||
form = form_fixture(%{presentation_file_id: presentation_file.id})
|
||||
assert Forms.get_form!(form.id) == form
|
||||
fetched_form = Forms.get_form!(form.id)
|
||||
|
||||
assert fetched_form.id == form.id
|
||||
assert fetched_form.position == form.position
|
||||
assert fetched_form.title == form.title
|
||||
assert fetched_form.fields == form.fields
|
||||
end
|
||||
|
||||
test "create_form/1 with valid data creates a form" do
|
||||
@@ -70,7 +82,9 @@ defmodule Claper.FormsTest do
|
||||
assert {:error, %Ecto.Changeset{}} =
|
||||
Forms.update_form(presentation_file.event_id, form, @invalid_attrs)
|
||||
|
||||
assert form == Forms.get_form!(form.id)
|
||||
fetched_form = Forms.get_form!(form.id)
|
||||
|
||||
assert fetched_form.title == form.title
|
||||
end
|
||||
|
||||
test "delete_form/2 deletes the form" do
|
||||
|
||||
@@ -14,14 +14,21 @@ defmodule Claper.PollsTest do
|
||||
presentation_file = presentation_file_fixture()
|
||||
poll = poll_fixture(%{presentation_file_id: presentation_file.id})
|
||||
|
||||
assert Polls.list_polls(presentation_file.id) == [poll]
|
||||
polls = Polls.list_polls(presentation_file.id)
|
||||
assert [%Poll{} | _] = polls
|
||||
assert length(polls) == 1
|
||||
assert hd(polls).id == poll.id
|
||||
end
|
||||
|
||||
test "list_polls_at_position/2 returns all polls from a presentation at a given position" do
|
||||
presentation_file = presentation_file_fixture()
|
||||
poll = poll_fixture(%{presentation_file_id: presentation_file.id, position: 5})
|
||||
|
||||
assert Polls.list_polls_at_position(presentation_file.id, 5) == [poll]
|
||||
polls = Polls.list_polls_at_position(presentation_file.id, 5)
|
||||
assert [%Poll{} | _] = polls
|
||||
assert length(polls) == 1
|
||||
assert hd(polls).id == poll.id
|
||||
assert hd(polls).position == 5
|
||||
end
|
||||
|
||||
test "get_poll!/1 returns the poll with given id" do
|
||||
@@ -31,7 +38,12 @@ defmodule Claper.PollsTest do
|
||||
poll_fixture(%{presentation_file_id: presentation_file.id})
|
||||
|> Claper.Polls.set_percentages()
|
||||
|
||||
assert Polls.get_poll!(poll.id) == poll
|
||||
fetched_poll = Polls.get_poll!(poll.id)
|
||||
|
||||
assert fetched_poll.id == poll.id
|
||||
assert fetched_poll.position == poll.position
|
||||
assert fetched_poll.poll_opts == poll.poll_opts
|
||||
assert fetched_poll.title == poll.title
|
||||
end
|
||||
|
||||
test "create_poll/1 with valid data creates a poll" do
|
||||
@@ -73,7 +85,12 @@ defmodule Claper.PollsTest do
|
||||
assert {:error, %Ecto.Changeset{}} =
|
||||
Polls.update_poll(presentation_file.event_id, poll, @invalid_attrs)
|
||||
|
||||
assert poll |> Claper.Polls.set_percentages() == Polls.get_poll!(poll.id)
|
||||
fetched_poll = Polls.get_poll!(poll.id)
|
||||
poll = poll |> Claper.Polls.set_percentages()
|
||||
|
||||
assert fetched_poll.poll_opts == poll.poll_opts
|
||||
assert fetched_poll.poll_votes == poll.poll_votes
|
||||
assert fetched_poll.title == poll.title
|
||||
end
|
||||
|
||||
test "delete_poll/2 deletes the poll" do
|
||||
|
||||
@@ -23,14 +23,7 @@ defmodule ClaperWeb.EventLiveTest do
|
||||
end
|
||||
|
||||
test "updates event in listing", %{conn: conn, presentation_file: presentation_file} do
|
||||
{:ok, index_live, _html} = live(conn, ~p"/events")
|
||||
|
||||
assert index_live
|
||||
|> element("#event-#{presentation_file.event.uuid} a", "Edit")
|
||||
|> render_click() =~
|
||||
"Edit"
|
||||
|
||||
assert_patch(index_live, ~p"/events/#{presentation_file.event.uuid}/edit")
|
||||
{:ok, index_live, _html} = live(conn, ~p"/events/#{presentation_file.event.uuid}/edit")
|
||||
|
||||
{:ok, conn} =
|
||||
index_live
|
||||
@@ -43,12 +36,7 @@ defmodule ClaperWeb.EventLiveTest do
|
||||
end
|
||||
|
||||
test "deletes event in listing", %{conn: conn, presentation_file: presentation_file} do
|
||||
{:ok, index_live, _html} = live(conn, ~p"/events")
|
||||
|
||||
assert index_live
|
||||
|> element("#event-#{presentation_file.event.uuid} a", "Edit")
|
||||
|> render_click() =~
|
||||
"Edit"
|
||||
{:ok, index_live, _html} = live(conn, ~p"/events/#{presentation_file.event.uuid}/edit")
|
||||
|
||||
{:ok, conn} =
|
||||
index_live
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
defmodule Lti_1p3.DataProviders.EctoProvider.TestHelpers do
|
||||
import Lti_1p3.Config
|
||||
|
||||
alias Lti_1p3.Test.Lti_1p3_User
|
||||
alias Lti_1p3.Jwk
|
||||
alias Lti_1p3.Tool.Registration
|
||||
alias Lti_1p3.Tool.Deployment
|
||||
|
||||
Mox.defmock(Lti_1p3.Test.MockHTTPoison, for: HTTPoison.Base)
|
||||
|
||||
def lti_1p3_user(attrs \\ %{}) do
|
||||
params =
|
||||
attrs
|
||||
|> Enum.into(%{
|
||||
id: 0,
|
||||
sub: "a6d5c443-1f51-4783-ba1a-7686ffe3b54a",
|
||||
name: "Ms Jane Marie Doe",
|
||||
given_name: "Jane",
|
||||
family_name: "Doe",
|
||||
middle_name: "Marie",
|
||||
picture: "https://platform.example.edu/jane.jpg",
|
||||
email: "jane#{System.unique_integer([:positive])}@platform.example.edu",
|
||||
locale: "en-US",
|
||||
platform_roles:
|
||||
"http://purl.imsglobal.org/vocab/lis/v2/system/person#User,http://purl.imsglobal.org/vocab/lis/v2/institution/person#Student",
|
||||
context_roles: "http://purl.imsglobal.org/vocab/lis/v2/membership#Learner"
|
||||
})
|
||||
|
||||
struct!(Lti_1p3_User, params)
|
||||
end
|
||||
|
||||
def jwk_fixture(attrs \\ %{}) do
|
||||
%{private_key: private_key} = Lti_1p3.KeyGenerator.generate_key_pair()
|
||||
|
||||
params =
|
||||
attrs
|
||||
|> Enum.into(%{
|
||||
pem: private_key,
|
||||
typ: "JWT",
|
||||
alg: "RS256",
|
||||
kid: UUID.uuid4(),
|
||||
active: true
|
||||
})
|
||||
|
||||
{:ok, jwk} = provider!().create_jwk(struct!(Jwk, params))
|
||||
|
||||
jwk
|
||||
end
|
||||
|
||||
def registration_fixture(%{tool_jwk_id: tool_jwk_id} = attrs) do
|
||||
params =
|
||||
attrs
|
||||
|> Enum.into(%{
|
||||
issuer: "https://lti-ri.imsglobal.org",
|
||||
client_id: "12345",
|
||||
key_set_url: "some key_set_url",
|
||||
auth_token_url: "some auth_token_url",
|
||||
auth_login_url: "some auth_login_url",
|
||||
auth_server: "some auth_server",
|
||||
tool_jwk_id: tool_jwk_id
|
||||
})
|
||||
|
||||
{:ok, registration} = provider!().create_registration(struct(Registration, params))
|
||||
|
||||
registration
|
||||
end
|
||||
|
||||
def deployment_fixture(
|
||||
%{deployment_id: deployment_id, registration_id: registration_id} = attrs
|
||||
) do
|
||||
params =
|
||||
attrs
|
||||
|> Enum.into(%{
|
||||
deployment_id: deployment_id,
|
||||
registration_id: registration_id
|
||||
})
|
||||
|
||||
{:ok, deployment} = provider!().create_deployment(struct(Deployment, params))
|
||||
|
||||
deployment
|
||||
end
|
||||
|
||||
def generate_id_token(jwk, kid, claims) do
|
||||
# create a signer
|
||||
signer =
|
||||
Joken.Signer.create("RS256", %{"pem" => jwk.pem}, %{
|
||||
"kid" => kid
|
||||
})
|
||||
|
||||
{:ok, claims} = Joken.generate_claims(%{}, claims)
|
||||
|
||||
Joken.generate_and_sign!(%{}, claims, signer)
|
||||
end
|
||||
|
||||
def all_default_claims() do
|
||||
%{}
|
||||
|> Map.merge(security_detail_data())
|
||||
|> Map.merge(user_detail_data())
|
||||
|> Map.merge(claims_data())
|
||||
|> Map.merge(example_extension_data())
|
||||
end
|
||||
|
||||
def security_detail_data() do
|
||||
%{
|
||||
"iss" => "https://lti-ri.imsglobal.org",
|
||||
"sub" => "a73d59affc5b2c4cd493",
|
||||
"aud" => "12345",
|
||||
"exp" => Timex.now() |> Timex.add(Timex.Duration.from_minutes(5)) |> Timex.to_unix(),
|
||||
"iat" => Timex.now() |> Timex.to_unix(),
|
||||
"nonce" => UUID.uuid4()
|
||||
}
|
||||
end
|
||||
|
||||
def user_detail_data() do
|
||||
%{
|
||||
"given_name" => "Chelsea",
|
||||
"family_name" => "Conroy",
|
||||
"middle_name" => "Reichel",
|
||||
"picture" => "http://example.org/Chelsea.jpg",
|
||||
"email" => "Chelsea.Conroy@example.org",
|
||||
"name" => "Chelsea Reichel Conroy",
|
||||
"locale" => "en-US"
|
||||
}
|
||||
end
|
||||
|
||||
def claims_data() do
|
||||
%{
|
||||
"https://purl.imsglobal.org/spec/lti-ags/claim/endpoint" => %{
|
||||
"lineitems" => "https://lti-ri.imsglobal.org/platforms/1237/contexts/10337/line_items",
|
||||
"scope" => [
|
||||
"https://purl.imsglobal.org/spec/lti-ags/scope/lineitem",
|
||||
"https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly"
|
||||
]
|
||||
},
|
||||
"https://purl.imsglobal.org/spec/lti-ces/claim/caliper-endpoint-service" => %{
|
||||
"caliper_endpoint_url" => "https://lti-ri.imsglobal.org/platforms/1237/sensors",
|
||||
"caliper_federated_session_id" => "urn:uuid:7bec5956c5297eacf382",
|
||||
"scopes" => ["https://purl.imsglobal.org/spec/lti-ces/v1p0/scope/send"]
|
||||
},
|
||||
"https://purl.imsglobal.org/spec/lti-nrps/claim/namesroleservice" => %{
|
||||
"context_memberships_url" =>
|
||||
"https://lti-ri.imsglobal.org/platforms/1237/contexts/10337/memberships",
|
||||
"service_versions" => ["2.0"]
|
||||
},
|
||||
"https://purl.imsglobal.org/spec/lti/claim/context" => %{
|
||||
"id" => "10337",
|
||||
"label" => "My Course",
|
||||
"title" => "My Course",
|
||||
"type" => ["Course"]
|
||||
},
|
||||
"https://purl.imsglobal.org/spec/lti/claim/custom" => %{
|
||||
"myCustomValue" => "123"
|
||||
},
|
||||
"https://purl.imsglobal.org/spec/lti/claim/deployment_id" => "1",
|
||||
"https://purl.imsglobal.org/spec/lti/claim/launch_presentation" => %{
|
||||
"document_target" => "iframe",
|
||||
"height" => 320,
|
||||
"return_url" => "https://lti-ri.imsglobal.org/platforms/1237/returns",
|
||||
"width" => 240
|
||||
},
|
||||
"https://purl.imsglobal.org/spec/lti/claim/message_type" => "LtiResourceLinkRequest",
|
||||
"https://purl.imsglobal.org/spec/lti/claim/resource_link" => %{
|
||||
"description" => "my course",
|
||||
"id" => "20052",
|
||||
"title" => "My Course"
|
||||
},
|
||||
"https://purl.imsglobal.org/spec/lti/claim/role_scope_mentor" => ["a62c52c02ba262003f5e"],
|
||||
"https://purl.imsglobal.org/spec/lti/claim/roles" => [
|
||||
"http://purl.imsglobal.org/vocab/lis/v2/membership#Learner",
|
||||
"http://purl.imsglobal.org/vocab/lis/v2/institution/person#Student",
|
||||
"http://purl.imsglobal.org/vocab/lis/v2/membership#Mentor"
|
||||
],
|
||||
"https://purl.imsglobal.org/spec/lti/claim/target_link_uri" =>
|
||||
"https://lti-ri.imsglobal.org/lti/tools/1193/launches",
|
||||
"https://purl.imsglobal.org/spec/lti/claim/tool_platform" => %{
|
||||
"contact_email" => "",
|
||||
"description" => "",
|
||||
"guid" => 1237,
|
||||
"name" => "lti-test",
|
||||
"product_family_code" => "",
|
||||
"url" => "",
|
||||
"version" => "1.0"
|
||||
},
|
||||
"https://purl.imsglobal.org/spec/lti/claim/version" => "1.3.0"
|
||||
}
|
||||
end
|
||||
|
||||
def example_extension_data() do
|
||||
%{
|
||||
"https://www.example.com/extension" => %{"color" => "violet"}
|
||||
}
|
||||
end
|
||||
|
||||
def mock_get_jwk_keys(jwk) do
|
||||
body =
|
||||
Jason.encode!(%{
|
||||
keys: [
|
||||
jwk.pem
|
||||
|> JOSE.JWK.from_pem()
|
||||
|> JOSE.JWK.to_public()
|
||||
|> JOSE.JWK.to_map()
|
||||
|> (fn {_kty, public_jwk} -> public_jwk end).()
|
||||
|> Map.put("typ", jwk.typ)
|
||||
|> Map.put("alg", jwk.alg)
|
||||
|> Map.put("kid", jwk.kid)
|
||||
|> Map.put("use", "sig")
|
||||
]
|
||||
})
|
||||
|
||||
{:ok, %HTTPoison.Response{status_code: 200, body: body}}
|
||||
end
|
||||
end
|
||||
@@ -53,7 +53,9 @@ defmodule Lti13.UsersTest do
|
||||
|
||||
{:ok, %User{} = user} = Users.create_user(attrs)
|
||||
|
||||
assert %User{id: id} = Users.get_user_by_sub(attrs.sub)
|
||||
assert %User{id: id} =
|
||||
Users.get_user_by_sub_and_registration_id(attrs.sub, attrs.registration_id)
|
||||
|
||||
assert id == user.id
|
||||
end
|
||||
|
||||
@@ -66,8 +68,7 @@ defmodule Lti13.UsersTest do
|
||||
name: "John Doe",
|
||||
roles: ["role1", "role2"],
|
||||
email: claper_user.email,
|
||||
client_id: registration.client_id,
|
||||
issuer: registration.issuer
|
||||
registration_id: registration.id
|
||||
}
|
||||
|
||||
assert {:ok, %User{} = user} = Users.get_or_create_user(attrs)
|
||||
@@ -89,8 +90,6 @@ defmodule Lti13.UsersTest do
|
||||
name: "John Doe",
|
||||
roles: ["role1", "role2"],
|
||||
email: claper_user.email,
|
||||
client_id: registration.client_id,
|
||||
issuer: registration.issuer,
|
||||
registration_id: registration.id,
|
||||
user_id: claper_user.id
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user