mirror of
https://github.com/ClaperCo/Claper.git
synced 2026-02-24 04:01:04 +01:00
Merge branch 'dev' into feature/event-list-ui-rework
This commit is contained in:
86
.env.sample
86
.env.sample
@@ -1,3 +1,5 @@
|
||||
# === Basic Phoenix configuration ===
|
||||
|
||||
BASE_URL=http://localhost:4000
|
||||
# SAME_SITE_COOKIE=Lax
|
||||
# SECURE_COOKIE=false
|
||||
@@ -6,51 +8,75 @@ DATABASE_URL=postgres://claper:claper@db:5432/claper
|
||||
SECRET_KEY_BASE=0LZiQBLw4WvqPlz4cz8RsHJlxNiSqM9B48y4ChyJ5v1oA0L/TPIqRjQNdPZN3iEG # Generate with `mix phx.gen.secret`
|
||||
# ⚠️ Don't use this exact value for SECRET_KEY_BASE or someone would be able to sign a cookie with user_id=1 and log in as the admin!
|
||||
|
||||
# Storage configuration
|
||||
|
||||
# === Storage configuration ===
|
||||
|
||||
PRESENTATION_STORAGE=local
|
||||
PRESENTATION_STORAGE_DIR=/app/uploads
|
||||
#MAX_FILE_SIZE_MB=15
|
||||
# MAX_FILE_SIZE_MB=15
|
||||
|
||||
# The standard AWS environment variables
|
||||
#S3_ACCESS_KEY_ID=xxx
|
||||
#S3_SECRET_ACCESS_KEY=xxx
|
||||
#S3_REGION=eu-west-3
|
||||
#S3_BUCKET=xxx
|
||||
# == Standard AWS environment variables
|
||||
|
||||
# If you're using an alternative S3-compatible service, port optional
|
||||
#S3_SCHEME=https://
|
||||
#S3_HOST=www.example.com
|
||||
#S3_PORT=443
|
||||
# S3_ACCESS_KEY_ID=xxx
|
||||
# S3_SECRET_ACCESS_KEY=xxx
|
||||
# S3_REGION=eu-west-3
|
||||
# S3_BUCKET=xxx
|
||||
|
||||
# If the public S3-compatible URL is different from the one used to write data
|
||||
#S3_PUBLIC_URL=https://www.example.com
|
||||
# == If you're using an alternative S3-compatible service, port optional
|
||||
|
||||
# Mail configuration
|
||||
# S3_SCHEME=https://
|
||||
# S3_HOST=www.example.com
|
||||
# S3_PORT=443
|
||||
|
||||
MAIL_TRANSPORT=local
|
||||
# == If the public S3-compatible URL is different from the one used to write data
|
||||
|
||||
# S3_PUBLIC_URL=https://www.example.com
|
||||
|
||||
|
||||
# === Mail configuration ===
|
||||
|
||||
MAIL_TRANSPORT=local # smtp or postmark, anything else uses the local adapter
|
||||
MAIL_FROM=noreply@claper.co
|
||||
MAIL_FROM_NAME=Claper
|
||||
|
||||
#SMTP_RELAY=xx.example.com
|
||||
#SMTP_USERNAME=johndoe@example.com
|
||||
#SMTP_PASSWORD=xxx
|
||||
#SMTP_PORT=465
|
||||
# == Use the following if MAIL_TRANSPORT=smtp
|
||||
|
||||
#ENABLE_MAILBOX_ROUTE=false
|
||||
#MAILBOX_USER=admin
|
||||
#MAILBOX_PASSWORD=admin
|
||||
# SMTP_RELAY=smtp.example.com
|
||||
# SMTP_PORT=465
|
||||
# SMTP_RETRIES=1
|
||||
# SMTP_NO_MX_LOOKUPS=false
|
||||
|
||||
# Claper configuration
|
||||
# SMTP_AUTH=always # if_available, always or never
|
||||
# SMTP_USERNAME=username
|
||||
# SMTP_PASSWORD=xxx
|
||||
|
||||
#ENABLE_ACCOUNT_CREATION=true
|
||||
#EMAIL_CONFIRMATION=true
|
||||
#ALLOW_UNLINK_EXTERNAL_PROVIDER=false
|
||||
#LOGOUT_REDIRECT_URL=https://google.com
|
||||
#GS_JPG_RESOLUTION=300x300
|
||||
#LANGUAGES=en,fr,es,it,nl,de
|
||||
# SMTP_SSL=false
|
||||
# SMTP_TLS=if_available # if_available, always or never
|
||||
# SMTP_SSL_DEPTH=2
|
||||
# SMTP_SSL_SERVER=*.example.com
|
||||
|
||||
# OIDC configuration
|
||||
# == Use the following if MAIL_TRANSPORT=postmark
|
||||
|
||||
# POSTMARK_API_KEY=xxx
|
||||
|
||||
# == Dev mailbox
|
||||
|
||||
# ENABLE_MAILBOX_ROUTE=false
|
||||
# MAILBOX_USER=admin
|
||||
# MAILBOX_PASSWORD=admin
|
||||
|
||||
|
||||
# === Claper configuration ===
|
||||
|
||||
# ENABLE_ACCOUNT_CREATION=true
|
||||
# EMAIL_CONFIRMATION=true
|
||||
# ALLOW_UNLINK_EXTERNAL_PROVIDER=false
|
||||
# LOGOUT_REDIRECT_URL=https://google.com
|
||||
# GS_JPG_RESOLUTION=300x300
|
||||
# LANGUAGES=en,fr,es,it,nl,de
|
||||
|
||||
|
||||
# === OIDC configuration ===
|
||||
|
||||
# OIDC_PROVIDER_NAME="OpenID"
|
||||
# OIDC_ISSUER=https://my-idp.example/application/o/claper/
|
||||
|
||||
17
CHANGELOG.md
17
CHANGELOG.md
@@ -1,4 +1,19 @@
|
||||
### v.2.4.1
|
||||
### v.2.5.0
|
||||
|
||||
### Security
|
||||
|
||||
- Fix stored XSS vulnerability in custom embed iframes via input sanitization with attribute whitelisting
|
||||
- Fix XSS vulnerability in URL link formatting by escaping user-submitted URLs
|
||||
- Fix IDOR on form export endpoint by adding authorization check
|
||||
- Fix atom exhaustion DoS by replacing `String.to_atom/1` on user input with explicit whitelists (8 locations)
|
||||
- Add rate limiting on authentication endpoints using Hammer 7.0
|
||||
|
||||
### Fixes and improvements
|
||||
|
||||
- Fix form submission crash for anonymous attendees
|
||||
- Improve SMTP config and handling (#197)
|
||||
- Fix presentation slides URL (#200)
|
||||
- Fix custom S3 endpoint (#199)
|
||||
|
||||
### v.2.4.0
|
||||
|
||||
|
||||
@@ -76,16 +76,6 @@ email_confirmation =
|
||||
pool_size = get_int_from_path_or_env(config_dir, "POOL_SIZE", 10)
|
||||
queue_target = get_int_from_path_or_env(config_dir, "QUEUE_TARGET", 5_000)
|
||||
|
||||
mail_transport = get_var_from_path_or_env(config_dir, "MAIL_TRANSPORT", "local")
|
||||
|
||||
smtp_relay = get_var_from_path_or_env(config_dir, "SMTP_RELAY", nil)
|
||||
smtp_username = get_var_from_path_or_env(config_dir, "SMTP_USERNAME", nil)
|
||||
smtp_password = get_var_from_path_or_env(config_dir, "SMTP_PASSWORD", nil)
|
||||
smtp_ssl = get_var_from_path_or_env(config_dir, "SMTP_SSL", "true") |> String.to_existing_atom()
|
||||
smtp_tls = get_var_from_path_or_env(config_dir, "SMTP_TLS", "always")
|
||||
smtp_auth = get_var_from_path_or_env(config_dir, "SMTP_AUTH", "always")
|
||||
smtp_port = get_int_from_path_or_env(config_dir, "SMTP_PORT", 25)
|
||||
|
||||
storage = get_var_from_path_or_env(config_dir, "PRESENTATION_STORAGE", "local")
|
||||
if storage not in ["local", "s3"], do: raise("Invalid PRESENTATION_STORAGE value #{storage}")
|
||||
|
||||
@@ -222,26 +212,45 @@ config :claper, ClaperWeb.MailboxGuard,
|
||||
get_var_from_path_or_env(config_dir, "ENABLE_MAILBOX_ROUTE", "false")
|
||||
|> String.to_existing_atom()
|
||||
|
||||
case mail_transport do
|
||||
case get_var_from_path_or_env(config_dir, "MAIL_TRANSPORT", "local") do
|
||||
"smtp" ->
|
||||
relay = get_var_from_path_or_env(config_dir, "SMTP_RELAY", nil)
|
||||
ssl = get_var_from_path_or_env(config_dir, "SMTP_SSL", "true")
|
||||
depth = get_int_from_path_or_env(config_dir, "SMTP_SSL_DEPTH", 2)
|
||||
|
||||
server =
|
||||
get_var_from_path_or_env(config_dir, "SMTP_SSL_SERVER", relay)
|
||||
|> to_charlist()
|
||||
|
||||
config :claper, Claper.Mailer,
|
||||
adapter: Swoosh.Adapters.Mua,
|
||||
relay: smtp_relay,
|
||||
port: smtp_port
|
||||
|
||||
cond do
|
||||
smtp_username && smtp_password ->
|
||||
config :claper, Claper.Mailer, auth: [username: smtp_username, password: smtp_password]
|
||||
|
||||
smtp_username || smtp_password ->
|
||||
raise ArgumentError, """
|
||||
Both SMTP_USERNAME and SMTP_PASSWORD must be set for SMTP authentication.
|
||||
Please provide values for both environment variables.
|
||||
"""
|
||||
|
||||
true ->
|
||||
nil
|
||||
end
|
||||
adapter: Swoosh.Adapters.SMTP,
|
||||
relay: relay,
|
||||
port: get_int_from_path_or_env(config_dir, "SMTP_PORT", 465),
|
||||
auth: get_var_from_path_or_env(config_dir, "SMTP_AUTH", "always"),
|
||||
username: get_var_from_path_or_env(config_dir, "SMTP_USERNAME", ""),
|
||||
password: get_var_from_path_or_env(config_dir, "SMTP_PASSWORD", ""),
|
||||
retries: get_int_from_path_or_env(config_dir, "SMTP_RETRIES", 1),
|
||||
no_mx_lookups: get_var_from_path_or_env(config_dir, "SMTP_NO_MX_LOOKUPS", "false"),
|
||||
ssl: ssl,
|
||||
sockopts:
|
||||
if(ssl == "true",
|
||||
do: [
|
||||
versions: [:"tlsv1.3", :"tlsv1.2"],
|
||||
verify: :verify_peer,
|
||||
cacerts: :public_key.cacerts_get(),
|
||||
depth: depth,
|
||||
server_name_indication: server
|
||||
],
|
||||
else: []
|
||||
),
|
||||
tls: get_var_from_path_or_env(config_dir, "SMTP_TLS", "if_available"),
|
||||
tls_options: [
|
||||
versions: [:"tlsv1.3", :"tlsv1.2"],
|
||||
verify: :verify_peer,
|
||||
cacerts: :public_key.cacerts_get(),
|
||||
depth: depth,
|
||||
server_name_indication: server
|
||||
]
|
||||
|
||||
config :swoosh, :api_client, false
|
||||
|
||||
@@ -250,7 +259,7 @@ case mail_transport do
|
||||
adapter: Swoosh.Adapters.Postmark,
|
||||
api_key: get_var_from_path_or_env(config_dir, "POSTMARK_API_KEY", nil)
|
||||
|
||||
config :swoosh, :api_client, Swoosh.ApiClient.Hackney
|
||||
config :swoosh, :api_client, Swoosh.ApiClient.Finch
|
||||
|
||||
_ ->
|
||||
config :claper, Claper.Mailer, adapter: Swoosh.Adapters.Local
|
||||
@@ -267,5 +276,3 @@ if s3_scheme && s3_host do
|
||||
config :ex_aws,
|
||||
s3: [scheme: s3_scheme, host: s3_host, port: s3_port]
|
||||
end
|
||||
|
||||
config :swoosh, :api_client, Swoosh.ApiClient.Finch
|
||||
|
||||
@@ -19,6 +19,8 @@ defmodule Claper.Application do
|
||||
ClaperWeb.Telemetry,
|
||||
# Start the PubSub system
|
||||
{Phoenix.PubSub, name: Claper.PubSub},
|
||||
# Start the rate limiter before the endpoint accepts requests
|
||||
Claper.RateLimit,
|
||||
# Start the Endpoint (http/https)
|
||||
ClaperWeb.Presence,
|
||||
ClaperWeb.Endpoint,
|
||||
|
||||
@@ -97,9 +97,62 @@ defmodule Claper.Embeds.Embed do
|
||||
|> validate_format(:content, ~r/<iframe.*?<\/iframe>/s,
|
||||
message: gettext("Please enter valid HTML content with an iframe tag")
|
||||
)
|
||||
|> sanitize_custom_embed()
|
||||
|
||||
_ ->
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
@allowed_iframe_attrs ~w(src width height frameborder allow allowfullscreen style title loading referrerpolicy sandbox)
|
||||
|
||||
defp sanitize_custom_embed(%{valid?: false} = changeset), do: changeset
|
||||
|
||||
defp sanitize_custom_embed(changeset) do
|
||||
content = get_field(changeset, :content)
|
||||
|
||||
case Regex.run(~r/<iframe\s[^>]*?(?:\/>|>[\s\S]*?<\/iframe>)/i, content) do
|
||||
[iframe_tag] ->
|
||||
put_change(changeset, :content, sanitize_iframe_tag(iframe_tag))
|
||||
|
||||
_ ->
|
||||
add_error(
|
||||
changeset,
|
||||
:content,
|
||||
gettext("Please enter valid HTML content with an iframe tag")
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@allowed_boolean_attrs ~w(allowfullscreen sandbox)
|
||||
|
||||
defp sanitize_iframe_tag(iframe_tag) do
|
||||
# Extract key="value" attributes
|
||||
value_attrs =
|
||||
Regex.scan(~r/([\w-]+)\s*=\s*(?:"([^"]*?)"|'([^']*?)')/i, iframe_tag)
|
||||
|> Enum.filter(fn [_, name | _] -> String.downcase(name) in @allowed_iframe_attrs end)
|
||||
|> Enum.reject(fn [_, name, value | _] ->
|
||||
String.downcase(name) == "src" and String.trim(value) =~ ~r/^javascript:/i
|
||||
end)
|
||||
|> Enum.map(fn [_, name, value | _rest] ->
|
||||
~s(#{String.downcase(name)}="#{String.replace(value, "\"", """)}")
|
||||
end)
|
||||
|
||||
# Extract standalone boolean attributes (e.g., allowfullscreen)
|
||||
value_attr_names =
|
||||
Regex.scan(~r/([\w-]+)\s*=/i, iframe_tag)
|
||||
|> Enum.map(fn [_, name] -> String.downcase(name) end)
|
||||
|> MapSet.new()
|
||||
|
||||
boolean_attrs =
|
||||
Regex.scan(~r/\s([\w-]+)(?=[\s>\/])/i, iframe_tag)
|
||||
|> Enum.map(fn [_, name] -> String.downcase(name) end)
|
||||
|> Enum.filter(&(&1 in @allowed_boolean_attrs))
|
||||
|> Enum.reject(&MapSet.member?(value_attr_names, &1))
|
||||
|> Enum.uniq()
|
||||
|
||||
all_attrs = Enum.join(value_attrs ++ boolean_attrs, " ")
|
||||
|
||||
if all_attrs == "", do: "<iframe></iframe>", else: "<iframe #{all_attrs}></iframe>"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -324,6 +324,9 @@ defmodule Claper.Forms do
|
||||
nil -> broadcast({:ok, r, event_uuid}, :form_submit_created)
|
||||
_form_submit -> broadcast({:ok, r, event_uuid}, :form_submit_updated)
|
||||
end
|
||||
|
||||
{:error, changeset} ->
|
||||
{:error, changeset}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -344,6 +347,7 @@ defmodule Claper.Forms do
|
||||
|> Repo.delete()
|
||||
|> case do
|
||||
{:ok, r} -> broadcast({:ok, r, event_uuid}, :form_submit_deleted)
|
||||
{:error, changeset} -> {:error, changeset}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -25,6 +25,18 @@ defmodule Claper.Forms.FormSubmit do
|
||||
def changeset(form_submit, attrs) do
|
||||
form_submit
|
||||
|> cast(attrs, [:attendee_identifier, :user_id, :form_id, :response])
|
||||
|> validate_required([:form_id, :user_id, :response])
|
||||
|> validate_required([:form_id, :response])
|
||||
|> validate_user_or_attendee()
|
||||
end
|
||||
|
||||
defp validate_user_or_attendee(changeset) do
|
||||
user_id = get_field(changeset, :user_id)
|
||||
attendee_identifier = get_field(changeset, :attendee_identifier)
|
||||
|
||||
if is_nil(user_id) and is_nil(attendee_identifier) do
|
||||
add_error(changeset, :user_id, "either user_id or attendee_identifier must be present")
|
||||
else
|
||||
changeset
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -96,6 +96,10 @@ defmodule Claper.Presentations do
|
||||
Returns a list of JPG slide URLs for a given presentation `hash` and
|
||||
`length`. See also `get_slide_urls/1`.
|
||||
"""
|
||||
def get_slide_urls(hash, length)
|
||||
|
||||
def get_slide_urls(nil, _), do: []
|
||||
|
||||
def get_slide_urls(hash, length) when is_binary(hash) and is_integer(length) do
|
||||
config = Application.get_env(:claper, :presentations)
|
||||
|
||||
|
||||
3
lib/claper/rate_limit.ex
Normal file
3
lib/claper/rate_limit.ex
Normal file
@@ -0,0 +1,3 @@
|
||||
defmodule Claper.RateLimit do
|
||||
use Hammer, backend: :ets
|
||||
end
|
||||
@@ -30,7 +30,14 @@ defmodule Claper.Tasks.Converter do
|
||||
|
||||
IO.puts("Starting conversion for #{hash}... (copy: #{is_copy})")
|
||||
|
||||
file_to_pdf(String.to_atom(ext), path, file)
|
||||
ext_atom =
|
||||
case ext do
|
||||
"ppt" -> :ppt
|
||||
"pptx" -> :pptx
|
||||
other -> other
|
||||
end
|
||||
|
||||
file_to_pdf(ext_atom, path, file)
|
||||
|> pdf_to_jpg(path, presentation, user_id)
|
||||
|> jpg_upload(hash, path, presentation, user_id, is_copy)
|
||||
end
|
||||
|
||||
@@ -10,20 +10,27 @@ defmodule ClaperWeb.StatController do
|
||||
@doc """
|
||||
Exports form submissions as a CSV file.
|
||||
"""
|
||||
def export_form(conn, %{"form_id" => form_id}) do
|
||||
form = Forms.get_form!(form_id, [:form_submits])
|
||||
headers = form.fields |> Enum.map(& &1.name)
|
||||
def export_form(%{assigns: %{current_user: current_user}} = conn, %{"form_id" => form_id}) do
|
||||
with form <- Forms.get_form!(form_id, [:form_submits]),
|
||||
presentation_file <-
|
||||
Presentations.get_presentation_file!(form.presentation_file_id, [:event]),
|
||||
event <- presentation_file.event,
|
||||
:ok <- authorize_event_access(current_user, event) do
|
||||
headers = form.fields |> Enum.map(& &1.name)
|
||||
|
||||
data =
|
||||
form.form_submits
|
||||
|> Enum.map(fn submit ->
|
||||
form.fields
|
||||
|> Enum.map(fn field ->
|
||||
Map.get(submit.response, field.name, "")
|
||||
data =
|
||||
form.form_submits
|
||||
|> Enum.map(fn submit ->
|
||||
form.fields
|
||||
|> Enum.map(fn field ->
|
||||
Map.get(submit.response, field.name, "")
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
export_as_csv(conn, headers, data, "form-#{sanitize(form.title)}")
|
||||
export_as_csv(conn, headers, data, "form-#{sanitize(form.title)}")
|
||||
else
|
||||
:unauthorized -> send_resp(conn, 403, "Forbidden")
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -6,8 +6,10 @@ defmodule ClaperWeb.Helpers do
|
||||
|> String.split(url_regex, include_captures: true)
|
||||
|> Enum.map(fn
|
||||
"http" <> _rest = url ->
|
||||
escaped = url |> Phoenix.HTML.html_escape() |> Phoenix.HTML.safe_to_string()
|
||||
|
||||
Phoenix.HTML.raw(
|
||||
~s(<a href="#{url}" target="_blank" class="cursor-pointer text-primary-500 hover:underline font-medium">#{url}</a>)
|
||||
~s(<a href="#{escaped}" target="_blank" class="cursor-pointer text-primary-500 hover:underline font-medium">#{escaped}</a>)
|
||||
)
|
||||
|
||||
text ->
|
||||
|
||||
@@ -34,7 +34,13 @@ defmodule ClaperWeb.AdminLive.DashboardLive do
|
||||
|
||||
@impl true
|
||||
def handle_event("change_period", %{"period" => period}, socket) do
|
||||
period_atom = String.to_atom(period)
|
||||
period_atom =
|
||||
case period do
|
||||
"day" -> :day
|
||||
"week" -> :week
|
||||
"month" -> :month
|
||||
_ -> :day
|
||||
end
|
||||
|
||||
days_back =
|
||||
case period_atom do
|
||||
|
||||
@@ -231,10 +231,16 @@ defmodule ClaperWeb.AdminLive.TableActionsComponent do
|
||||
action = Enum.find(socket.assigns.dropdown_actions, &(&1.key == action_key))
|
||||
|
||||
if action do
|
||||
send(
|
||||
self(),
|
||||
{:table_action, String.to_atom(action_key), socket.assigns.item, socket.assigns.item_id}
|
||||
)
|
||||
try do
|
||||
action_atom = String.to_existing_atom(action_key)
|
||||
|
||||
send(
|
||||
self(),
|
||||
{:table_action, action_atom, socket.assigns.item, socket.assigns.item_id}
|
||||
)
|
||||
rescue
|
||||
ArgumentError -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
{:noreply, assign(socket, dropdown_open: false)}
|
||||
|
||||
@@ -61,7 +61,7 @@ defmodule ClaperWeb.EventLive.FormComponent do
|
||||
form={f}
|
||||
labelClass="text-white"
|
||||
fieldClass="bg-gray-700 text-white"
|
||||
key={String.to_atom(field.name)}
|
||||
key={safe_field_atom(field.name)}
|
||||
name={field.name}
|
||||
required={field.required}
|
||||
value={
|
||||
@@ -75,7 +75,7 @@ defmodule ClaperWeb.EventLive.FormComponent do
|
||||
form={f}
|
||||
labelClass="text-white"
|
||||
fieldClass="bg-gray-700 text-white"
|
||||
key={String.to_atom(field.name)}
|
||||
key={safe_field_atom(field.name)}
|
||||
name={field.name}
|
||||
required={field.required}
|
||||
value={
|
||||
@@ -148,6 +148,9 @@ defmodule ClaperWeb.EventLive.FormComponent do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:current_form_submit, form_submit)}
|
||||
|
||||
{:error, _changeset} ->
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -167,9 +170,21 @@ defmodule ClaperWeb.EventLive.FormComponent do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:current_form_submit, form_submit)}
|
||||
|
||||
{:error, _changeset} ->
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
defp safe_field_atom(name) when is_binary(name) do
|
||||
String.to_existing_atom(name)
|
||||
rescue
|
||||
ArgumentError ->
|
||||
if Regex.match?(~r/^[\w]{1,100}$/, name),
|
||||
do: String.to_atom(name),
|
||||
else: :invalid_field
|
||||
end
|
||||
|
||||
def toggle_form(js \\ %JS{}) do
|
||||
js
|
||||
|> JS.toggle(
|
||||
|
||||
@@ -711,36 +711,42 @@ defmodule ClaperWeb.EventLive.Manage do
|
||||
|
||||
@impl true
|
||||
def handle_event("list-tab", %{"tab" => tab}, socket) do
|
||||
socket = assign(socket, :list_tab, String.to_atom(tab))
|
||||
|
||||
socket =
|
||||
{tab_atom, socket} =
|
||||
case tab do
|
||||
"posts" ->
|
||||
socket
|
||||
|> stream(:posts, list_all_posts(socket, socket.assigns.event.uuid), reset: true)
|
||||
{:posts,
|
||||
stream(socket, :posts, list_all_posts(socket, socket.assigns.event.uuid), reset: true)}
|
||||
|
||||
"questions" ->
|
||||
socket
|
||||
|> stream(:questions, list_all_questions(socket, socket.assigns.event.uuid),
|
||||
reset: true
|
||||
)
|
||||
{:questions,
|
||||
stream(socket, :questions, list_all_questions(socket, socket.assigns.event.uuid),
|
||||
reset: true
|
||||
)}
|
||||
|
||||
"forms" ->
|
||||
stream(
|
||||
socket,
|
||||
:form_submits,
|
||||
list_form_submits(socket, socket.assigns.event.presentation_file.id),
|
||||
reset: true
|
||||
)
|
||||
{:forms,
|
||||
stream(
|
||||
socket,
|
||||
:form_submits,
|
||||
list_form_submits(socket, socket.assigns.event.presentation_file.id),
|
||||
reset: true
|
||||
)}
|
||||
|
||||
"pinned_posts" ->
|
||||
socket
|
||||
|> stream(:pinned_posts, list_pinned_posts(socket, socket.assigns.event.uuid),
|
||||
reset: true
|
||||
)
|
||||
{:pinned_posts,
|
||||
stream(
|
||||
socket,
|
||||
:pinned_posts,
|
||||
list_pinned_posts(socket, socket.assigns.event.uuid),
|
||||
reset: true
|
||||
)}
|
||||
|
||||
_ ->
|
||||
{:posts,
|
||||
stream(socket, :posts, list_all_posts(socket, socket.assigns.event.uuid), reset: true)}
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
{:noreply, assign(socket, :list_tab, tab_atom)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
@@ -953,7 +959,13 @@ defmodule ClaperWeb.EventLive.Manage do
|
||||
end
|
||||
|
||||
defp list_all_questions(_socket, event_id, sort \\ "date") do
|
||||
Claper.Posts.list_questions(event_id, [:event, :reactions], String.to_atom(sort))
|
||||
sort_atom =
|
||||
case sort do
|
||||
"likes" -> :likes
|
||||
_ -> :date
|
||||
end
|
||||
|
||||
Claper.Posts.list_questions(event_id, [:event, :reactions], sort_atom)
|
||||
|> Enum.filter(&(ClaperWeb.Helpers.body_without_links(&1.body) =~ "?"))
|
||||
end
|
||||
|
||||
|
||||
@@ -7,6 +7,19 @@ defmodule ClaperWeb.EventLive.Show do
|
||||
|
||||
on_mount(ClaperWeb.AttendeeLiveAuth)
|
||||
|
||||
@global_react_types %{
|
||||
"heart" => :heart,
|
||||
"clap" => :clap,
|
||||
"hundred" => :hundred,
|
||||
"raisehand" => :raisehand
|
||||
}
|
||||
|
||||
@reaction_fields %{
|
||||
like: {:like_count, :like_posts},
|
||||
love: {:love_count, :love_posts},
|
||||
lol: {:lol_count, :lol_posts}
|
||||
}
|
||||
|
||||
@impl true
|
||||
def mount(%{"code" => code}, session, socket) do
|
||||
with %{"locale" => locale} <- session do
|
||||
@@ -422,13 +435,19 @@ defmodule ClaperWeb.EventLive.Show do
|
||||
%{"type" => type},
|
||||
socket
|
||||
) do
|
||||
Phoenix.PubSub.broadcast(
|
||||
Claper.PubSub,
|
||||
"event:#{socket.assigns.event.uuid}",
|
||||
{:react, String.to_atom(type)}
|
||||
)
|
||||
case Map.get(@global_react_types, type) do
|
||||
nil ->
|
||||
{:noreply, socket}
|
||||
|
||||
{:noreply, socket}
|
||||
type_atom ->
|
||||
Phoenix.PubSub.broadcast(
|
||||
Claper.PubSub,
|
||||
"event:#{socket.assigns.event.uuid}",
|
||||
{:react, type_atom}
|
||||
)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
@@ -752,8 +771,7 @@ defmodule ClaperWeb.EventLive.Show do
|
||||
defp add_reaction(socket, post_id, params, type) do
|
||||
with post <- Posts.get_post!(post_id, [:event]),
|
||||
{:ok, _} <- Posts.create_reaction(Map.merge(params, %{post: post})) do
|
||||
count_field = String.to_atom("#{type}_count")
|
||||
posts_field = String.to_atom("#{type}_posts")
|
||||
{count_field, posts_field} = @reaction_fields[type]
|
||||
|
||||
{:ok, _} = Posts.update_post(post, %{count_field => Map.get(post, count_field) + 1})
|
||||
update(socket, posts_field, fn posts -> [post.id | posts] end)
|
||||
@@ -763,8 +781,7 @@ defmodule ClaperWeb.EventLive.Show do
|
||||
defp remove_reaction(socket, post_id, params, type) do
|
||||
with post <- Posts.get_post!(post_id, [:event]),
|
||||
{:ok, _} <- Posts.delete_reaction(Map.merge(params, %{post: post})) do
|
||||
count_field = String.to_atom("#{type}_count")
|
||||
posts_field = String.to_atom("#{type}_posts")
|
||||
{count_field, posts_field} = @reaction_fields[type]
|
||||
|
||||
{:ok, _} = Posts.update_post(post, %{count_field => Map.get(post, count_field) - 1})
|
||||
update(socket, posts_field, fn posts -> List.delete(posts, post.id) end)
|
||||
|
||||
34
lib/claper_web/plugs/rate_limit_plug.ex
Normal file
34
lib/claper_web/plugs/rate_limit_plug.ex
Normal file
@@ -0,0 +1,34 @@
|
||||
defmodule ClaperWeb.Plugs.RateLimitPlug do
|
||||
@moduledoc """
|
||||
Plug for rate limiting requests based on client IP address.
|
||||
|
||||
## Usage
|
||||
|
||||
plug ClaperWeb.Plugs.RateLimitPlug, max_requests: 10, interval_ms: 60_000, prefix: "login"
|
||||
"""
|
||||
import Plug.Conn
|
||||
|
||||
def init(opts) do
|
||||
%{
|
||||
max_requests: Keyword.get(opts, :max_requests, 10),
|
||||
interval_ms: Keyword.get(opts, :interval_ms, 60_000),
|
||||
prefix: Keyword.get(opts, :prefix, "default")
|
||||
}
|
||||
end
|
||||
|
||||
def call(conn, %{max_requests: max_requests, interval_ms: interval_ms, prefix: prefix}) do
|
||||
ip = conn.remote_ip |> :inet.ntoa() |> to_string()
|
||||
key = "#{prefix}:#{ip}"
|
||||
|
||||
case Claper.RateLimit.hit(key, interval_ms, max_requests) do
|
||||
{:allow, _count} ->
|
||||
conn
|
||||
|
||||
{:deny, _retry_after} ->
|
||||
conn
|
||||
|> put_resp_content_type("text/plain")
|
||||
|> send_resp(429, "Too Many Requests")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -35,6 +35,13 @@ defmodule ClaperWeb.Router do
|
||||
plug(:accepts, ["json"])
|
||||
end
|
||||
|
||||
pipeline :rate_limit_auth do
|
||||
plug ClaperWeb.Plugs.RateLimitPlug,
|
||||
max_requests: 10,
|
||||
interval_ms: 60_000,
|
||||
prefix: "auth"
|
||||
end
|
||||
|
||||
# Manage attendee_identifier in requests
|
||||
pipeline :attendee_registration do
|
||||
plug(:attendee_identifier)
|
||||
@@ -120,7 +127,7 @@ defmodule ClaperWeb.Router do
|
||||
|
||||
## Authentication routes
|
||||
scope "/", ClaperWeb do
|
||||
pipe_through([:browser, :redirect_if_user_is_authenticated])
|
||||
pipe_through([:browser, :redirect_if_user_is_authenticated, :rate_limit_auth])
|
||||
|
||||
get("/users/register", UserRegistrationController, :new)
|
||||
post("/users/register", UserRegistrationController, :create)
|
||||
|
||||
7
mix.exs
7
mix.exs
@@ -1,7 +1,7 @@
|
||||
defmodule Claper.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
@version "2.4.1"
|
||||
@version "2.5.0"
|
||||
|
||||
def project do
|
||||
[
|
||||
@@ -95,6 +95,7 @@ defmodule Claper.MixProject do
|
||||
{:esbuild, "~> 0.10", runtime: Mix.env() == :dev},
|
||||
{:dart_sass, "~> 0.7", runtime: Mix.env() == :dev},
|
||||
{:swoosh, "~> 1.19"},
|
||||
{:gen_smtp, "~> 1.3"},
|
||||
{:finch, "~> 0.19"},
|
||||
{:telemetry_metrics, "~> 1.1"},
|
||||
{:telemetry_poller, "~> 1.2"},
|
||||
@@ -105,7 +106,6 @@ defmodule Claper.MixProject do
|
||||
{:hashids, "~> 2.1"},
|
||||
{:libcluster, "~> 3.5"},
|
||||
{:porcelain, "~> 2.0"},
|
||||
{:hackney, "~> 1.24"},
|
||||
{:csv, "~> 3.2"},
|
||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
||||
{:joken, "~> 2.6"},
|
||||
@@ -114,8 +114,7 @@ defmodule Claper.MixProject do
|
||||
{:uuid, "~> 1.1"},
|
||||
{:oidcc, "~> 3.5"},
|
||||
{:oban, "~> 2.19"},
|
||||
{:mua, "~> 0.2"},
|
||||
{:mail, "~> 0.5"},
|
||||
{:hammer, "~> 7.0"},
|
||||
{:tailwind, "~> 0.3", runtime: Mix.env() == :dev}
|
||||
]
|
||||
end
|
||||
|
||||
26
mix.lock
26
mix.lock
@@ -2,10 +2,7 @@
|
||||
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"},
|
||||
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
||||
"castore": {:hex, :castore, "1.0.14", "4582dd7d630b48cf5e1ca8d3d42494db51e406b7ba704e81fbd401866366896a", [:mix], [], "hexpm", "7bc1b65249d31701393edaaac18ec8398d8974d52c647b7904d01b964137b9f4"},
|
||||
"certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"},
|
||||
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
||||
"comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
|
||||
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
|
||||
"cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"},
|
||||
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
||||
"cowlib": {:hex, :cowlib, "2.15.0", "3c97a318a933962d1c12b96ab7c1d728267d2c523c25a5b57b0f93392b6e9e25", [:make, :rebar3], [], "hexpm", "4f00c879a64b4fe7c8fcb42a4281925e9ffdb928820b03c3ad325a617e857532"},
|
||||
@@ -26,36 +23,25 @@
|
||||
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
|
||||
"finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
|
||||
"floki": {:hex, :floki, "0.38.0", "62b642386fa3f2f90713f6e231da0fa3256e41ef1089f83b6ceac7a3fd3abf33", [:mix], [], "hexpm", "a5943ee91e93fb2d635b612caf5508e36d37548e84928463ef9dd986f0d1abd9"},
|
||||
"gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"},
|
||||
"gen_smtp": {:hex, :gen_smtp, "1.3.0", "62c3d91f0dcf6ce9db71bcb6881d7ad0d1d834c7f38c13fa8e952f4104a8442e", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "0b73fbf069864ecbce02fe653b16d3f35fd889d0fdd4e14527675565c39d84e6"},
|
||||
"gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"},
|
||||
"hackney": {:hex, :hackney, "1.24.1", "f5205a125bba6ed4587f9db3cc7c729d11316fa8f215d3e57ed1c067a9703fa9", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "f4a7392a0b53d8bbc3eb855bdcc919cd677358e65b2afd3840b5b3690c4c8a39"},
|
||||
"hammer": {:hex, :hammer, "7.2.0", "73113eca87f0fd20a6d3679c1182e8c4c1778266f61de4e9dc8c589dee156c30", [:mix], [], "hexpm", "c50fa865ddfe7b3d4f8a6941f56940679e02a9a1465b00668a95d140b101d828"},
|
||||
"hashids": {:hex, :hashids, "2.1.0", "aabbcc4f9fa0b460cc6ef629f5bcbc35e7e87b382fee79f9c50be40b86574288", [:mix], [], "hexpm", "172163b1642d415881ef1c6e1f1be12d4e92b0711d5bbbd8854f82a1ce32d60b"},
|
||||
"honeybadger": {:hex, :honeybadger, "0.18.1", "f61f71147d9e6ce8cdc6114e5df5eed4d2daa32dd15308267a1dacf5fa88b1e9", [:mix], [{:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:hackney, "~> 1.1", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.0.0 and < 2.0.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, ">= 1.0.0 and < 2.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "693e14b1b7d254dd75f977240c208d6b69cc1cbdd515bdd5b8b1738a1baf5fd5"},
|
||||
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
|
||||
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
|
||||
"httpoison": {:hex, :httpoison, "2.2.1", "87b7ed6d95db0389f7df02779644171d7319d319178f6680438167d7b69b1f3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "51364e6d2f429d80e14fe4b5f8e39719cacd03eb3f9a9286e61e216feac2d2df"},
|
||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
||||
"joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"},
|
||||
"jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"},
|
||||
"libcluster": {:hex, :libcluster, "3.5.0", "5ee4cfde4bdf32b2fef271e33ce3241e89509f4344f6c6a8d4069937484866ba", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ebf6561fcedd765a4cd43b4b8c04b1c87f4177b5fb3cbdfe40a780499d72f743"},
|
||||
"lti_1p3": {:hex, :lti_1p3, "0.6.0", "e896c56b0ae067b768fb5ce2c44db305b6d7bbc213fafd1bbb7da586629c6d9f", [:mix], [{:httpoison, "~> 2.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.3", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.2.0", [hex: :joken, repo: "hexpm", optional: false]}, {:timex, "~> 3.5", [hex: :timex, repo: "hexpm", optional: false]}, {:uuid, "~> 1.1", [hex: :uuid, repo: "hexpm", optional: false]}], "hexpm", "7c142ae8027913bbaa2267a28f448e734b1c5b2563e2aee2b1481632b84e35b3"},
|
||||
"lti_1p3_ecto_provider": {:hex, :lti_1p3_ecto_provider, "0.6.0", "7d8f3293b2dd32c243a55b5020ff0e37c5372ba8669bf82944b173d65f22a8e3", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:httpoison, "~> 2.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:lti_1p3, "~> 0.6.0", [hex: :lti_1p3, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:timex, "~> 3.5", [hex: :timex, repo: "hexpm", optional: false]}, {:uuid, "~> 1.1", [hex: :uuid, repo: "hexpm", optional: false]}], "hexpm", "b825e745ccd4dfb840bf1217056f535dd1c50ecbe673917c3b17efc4a64246d1"},
|
||||
"mail": {:hex, :mail, "0.5.1", "6383a61620aea24675c96e34b9019dede1bfc9a37ee10ce5a5cafcc7c5e48743", [:mix], [], "hexpm", "595144340b74f23d651ea2b4a72a896819940478d7425dfa302ea3b5c9041ec9"},
|
||||
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
|
||||
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
|
||||
"makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
|
||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
||||
"mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"},
|
||||
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
|
||||
"mua": {:hex, :mua, "0.2.4", "a9172ab0a1ac8732cf2699d739ceac3febcb9b4ffc540260ad2e32c0b6632af9", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "e7e4dacd5ad65f13e3542772e74a159c00bd2d5579e729e9bb72d2c73a266fb7"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
|
||||
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
|
||||
"oban": {:hex, :oban, "2.19.4", "045adb10db1161dceb75c254782f97cdc6596e7044af456a59decb6d06da73c1", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5fcc6219e6464525b808d97add17896e724131f498444a292071bf8991c99f97"},
|
||||
"oidcc": {:hex, :oidcc, "3.5.2", "91b0097a3fee86abb1aabfd80f2f910439688287408d916fed3535724b4fb897", [:mix, :rebar3], [{:igniter, "~> 0.5.50", [hex: :igniter, repo: "hexpm", optional: true]}, {:jose, "~> 1.11", [hex: :jose, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.3.1", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "474e7eddbf90ec4c8c50aeefe9a1ffb46ced72e6a11c2bb0c6eece9794821c2a"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
|
||||
"phoenix": {:hex, :phoenix, "1.7.21", "14ca4f1071a5f65121217d6b57ac5712d1857e40a0833aff7a691b7870fc9a3b", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "336dce4f86cba56fed312a7d280bf2282c720abb6074bdb1b61ec8095bdd0bc9"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.6.5", "c4ef322acd15a574a8b1a08eff0ee0a85e73096b53ce1403b6563709f15e1cea", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "26ec3208eef407f31b748cadd044045c6fd485fbff168e35963d2f9dfff28d4b"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"},
|
||||
@@ -70,12 +56,11 @@
|
||||
"plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.7.4", "729c752d17cf364e2b8da5bdb34fb5804f56251e88bb602aff48ae0bd8673d11", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9b85632bd7012615bae0a5d70084deb1b25d2bcbb32cab82d1e9a1e023168aa3"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
|
||||
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
|
||||
"porcelain": {:hex, :porcelain, "2.0.3", "2d77b17d1f21fed875b8c5ecba72a01533db2013bd2e5e62c6d286c029150fdc", [:mix], [], "hexpm", "dc996ab8fadbc09912c787c7ab8673065e50ea1a6245177b0c24569013d23620"},
|
||||
"postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"},
|
||||
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
|
||||
"req": {:hex, :req, "0.5.14", "521b449fa0bf275e6d034c05f29bec21789a0d6cd6f7a1c326c7bee642bf6e07", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "b7b15692071d556c73432c7797aa7e96b51d1a2db76f746b976edef95c930021"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
||||
"stripity_stripe": {:hex, :stripity_stripe, "2.13.0", "b9ea806fcf46e85232b75f2145c34770b17faa44c59cdd13ff493aaa6e84b4a9", [:mix], [{:hackney, "~> 1.15", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}, {:uri_query, "~> 0.1.2", [hex: :uri_query, repo: "hexpm", optional: false]}], "hexpm", "d6931ed9816552320f95428fd997edf15e99a913ca78fc4342d5516b98f42476"},
|
||||
"sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
|
||||
"swoosh": {:hex, :swoosh, "1.19.3", "02ad4455939f502386e4e1443d4de94c514995fd0e51b3cafffd6bd270ffe81c", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "04a10f8496786b744b84130e3510eb53ca51e769c39511b65023bdf4136b732f"},
|
||||
"tailwind": {:hex, :tailwind, "0.3.1", "a89d2835c580748c7a975ad7dd3f2ea5e63216dc16d44f9df492fbd12c094bed", [:mix], [], "hexpm", "98a45febdf4a87bc26682e1171acdedd6317d0919953c353fcd1b4f9f4b676a2"},
|
||||
@@ -83,11 +68,6 @@
|
||||
"telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
|
||||
"telemetry_poller": {:hex, :telemetry_poller, "1.2.0", "ba82e333215aed9dd2096f93bd1d13ae89d249f82760fcada0850ba33bac154b", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7216e21a6c326eb9aa44328028c34e9fd348fb53667ca837be59d0aa2a0156e8"},
|
||||
"telemetry_registry": {:hex, :telemetry_registry, "0.3.2", "701576890320be6428189bff963e865e8f23e0ff3615eade8f78662be0fc003c", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7ed191eb1d115a3034af8e1e35e4e63d5348851d556646d46ca3d1b4e16bab9"},
|
||||
"timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
|
||||
"tls_certificate_check": {:hex, :tls_certificate_check, "1.25.0", "702b1835fe718a52310509537392abd067dbe941ebc05fe72409d2b2f8061651", [:rebar3], [{:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "167343ccf50538cf2faf61a3f1460e749b3edf2ecef55516af2b5834362abcb1"},
|
||||
"tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
|
||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"},
|
||||
"uri_query": {:hex, :uri_query, "0.1.2", "ae35b83b472f3568c2c159eee3f3ccf585375d8a94fb5382db1ea3589e75c3b4", [:mix], [], "hexpm", "e3bc81816c98502c36498b9b2f239b89c71ce5eadfff7ceb2d6c0a2e6ae2ea0c"},
|
||||
"uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm", "c790593b4c3b601f5dc2378baae7efaf5b3d73c4c6456ba85759905be792f2ac"},
|
||||
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
||||
"websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"},
|
||||
|
||||
@@ -126,5 +126,62 @@ defmodule Claper.FormsTest do
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
test "create_or_update_form_submit/2 with attendee_identifier creates a form_submit" do
|
||||
presentation_file = presentation_file_fixture(%{}, [:event])
|
||||
f = form_fixture(%{presentation_file_id: presentation_file.id})
|
||||
|
||||
assert {:ok, %Claper.Forms.FormSubmit{} = form_submit} =
|
||||
Forms.create_or_update_form_submit(
|
||||
presentation_file.event.uuid,
|
||||
%{
|
||||
"attendee_identifier" => "test-attendee-123",
|
||||
"form_id" => f.id,
|
||||
"response" => %{"Name" => "Daniel"}
|
||||
}
|
||||
)
|
||||
|
||||
assert form_submit.attendee_identifier == "test-attendee-123"
|
||||
assert is_nil(form_submit.user_id)
|
||||
assert form_submit.form_id == f.id
|
||||
end
|
||||
|
||||
test "create_or_update_form_submit/2 with attendee_identifier updates existing form_submit" do
|
||||
presentation_file = presentation_file_fixture(%{}, [:event])
|
||||
f = form_fixture(%{presentation_file_id: presentation_file.id})
|
||||
|
||||
{:ok, _first_submit} =
|
||||
Forms.create_or_update_form_submit(
|
||||
presentation_file.event.uuid,
|
||||
%{
|
||||
"attendee_identifier" => "test-attendee-123",
|
||||
"form_id" => f.id,
|
||||
"response" => %{"Name" => "Daniel"}
|
||||
}
|
||||
)
|
||||
|
||||
assert {:ok, %Claper.Forms.FormSubmit{} = updated_submit} =
|
||||
Forms.create_or_update_form_submit(
|
||||
presentation_file.event.uuid,
|
||||
%{
|
||||
"attendee_identifier" => "test-attendee-123",
|
||||
"form_id" => f.id,
|
||||
"response" => %{"Name" => "Updated Name"}
|
||||
}
|
||||
)
|
||||
|
||||
assert updated_submit.response == %{"Name" => "Updated Name"}
|
||||
end
|
||||
|
||||
test "create_or_update_form_submit/2 without user_id or attendee_identifier returns error" do
|
||||
presentation_file = presentation_file_fixture(%{}, [:event])
|
||||
f = form_fixture(%{presentation_file_id: presentation_file.id})
|
||||
|
||||
assert {:error, %Ecto.Changeset{}} =
|
||||
Forms.create_form_submit(%{
|
||||
form_id: f.id,
|
||||
response: %{"Name" => "Daniel"}
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user