Skip to content

Commit 78df13d

Browse files
authored
Transfer Credential Ownership (#2946)
* Add credential transfer token validity configuration * Add credential transfer token support * Add credential transfer email notifications * Update user model for validations - Extract email validation functions - Move project access validation * Add credential transfer audit event * Add core transfer functionality * Add credential transfer UI * Unit tests and liveview tests - get projects by ids - credential transfer controller - initiate credential transfer, confirm credential transfer, revoke credential transfer - user changesets and validations - credential transfer modal * Revoke credential transfer * Tests for revoke credential transfer * Update CL * Remove duplicated test * Refactor code to address review change requests
1 parent d4228fd commit 78df13d

File tree

17 files changed

+1633
-217
lines changed

17 files changed

+1633
-217
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ and this project adheres to
1919

2020
- Enable project level concurrency limit
2121
[#2906](https://github.com/OpenFn/lightning/issues/2906)
22+
- Transfer credentials ownership to a project collaborator
23+
[#2820](https://github.com/OpenFn/lightning/issues/2820)
2224
- Delete unused snapshots on workorders retention cleanup
2325
[#1832](https://github.com/OpenFn/lightning/issues/1832)
2426

lib/lightning/accounts/user.ex

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,14 +164,18 @@ defmodule Lightning.Accounts.User do
164164
|> put_change(:role, :superuser)
165165
end
166166

167-
def validate_email(changeset) do
167+
def validate_email_format(changeset) do
168168
changeset
169169
|> validate_required(:email, message: "can't be blank")
170170
|> validate_format(:email, ~r/^[^\s]+@[^\s]+$/,
171171
message: "must have the @ sign and no spaces"
172172
)
173173
|> validate_length(:email, max: 160)
174174
|> update_change(:email, &String.downcase/1)
175+
end
176+
177+
def validate_email_exists(changeset) do
178+
changeset
175179
|> validate_change(:email, fn :email, email ->
176180
if Lightning.Repo.exists?(User |> where(email: ^email)) do
177181
[email: "has already been taken"]
@@ -181,6 +185,12 @@ defmodule Lightning.Accounts.User do
181185
end)
182186
end
183187

188+
def validate_email(changeset) do
189+
changeset
190+
|> validate_email_format()
191+
|> validate_email_exists()
192+
end
193+
184194
defp validate_password(changeset, opts) do
185195
changeset
186196
|> validate_required(:password, message: "can't be blank")

lib/lightning/accounts/user_notifier.ex

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,4 +415,54 @@ defmodule Lightning.Accounts.UserNotifier do
415415

416416
defp pluralize_with_s(1, string), do: string
417417
defp pluralize_with_s(_integer, string), do: "#{string}s"
418+
419+
@doc """
420+
Deliver instructions to confirm a credential transfer.
421+
"""
422+
def deliver_credential_transfer_confirmation_instructions(
423+
owner,
424+
receiver,
425+
credential,
426+
token
427+
) do
428+
validity_days = Lightning.Config.credential_transfer_token_validity_in_days()
429+
validity_text = format_validity_period(validity_days)
430+
431+
confirmation_url =
432+
url(~p"/credentials/transfer/#{credential.id}/#{receiver.id}/#{token}")
433+
434+
deliver(owner, "Confirm your credential transfer", """
435+
Hi #{owner.first_name},
436+
437+
Confirm your credential transfer to #{receiver.first_name} (#{receiver.email}) by visiting the link below:
438+
439+
#{confirmation_url}
440+
441+
Note that this link is only valid for #{validity_text}. If you didn't request this change, please ignore this.
442+
443+
OpenFn
444+
""")
445+
end
446+
447+
defp format_validity_period(days) do
448+
"#{days} #{pluralize_with_s(days, "day")}"
449+
end
450+
451+
def deliver_credential_transfer_notification(
452+
receiver,
453+
owner,
454+
credential
455+
) do
456+
credentials_url = url(~p"/credentials")
457+
458+
deliver(receiver, "You have received a credential", """
459+
Hi #{receiver.first_name},
460+
461+
#{owner.first_name} has transferred the credential \"#{credential.name}\" to you.
462+
463+
You can find it in your list of credentials by following this URL: #{credentials_url}
464+
465+
OpenFn
466+
""")
467+
end
418468
end

lib/lightning/accounts/user_token.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,9 @@ defmodule Lightning.Accounts.UserToken do
190190
defp days_for_context("confirm"), do: @confirm_validity_in_days
191191
defp days_for_context("reset_password"), do: @reset_password_validity_in_days
192192

193+
defp days_for_context("credential_transfer"),
194+
do: Lightning.Config.credential_transfer_token_validity_in_days()
195+
193196
@doc """
194197
Checks if the token is valid and returns its underlying lookup query.
195198

lib/lightning/config.ex

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,11 @@ defmodule Lightning.Config do
260260
defp ui_metrics_tracking_config do
261261
Application.get_env(:lightning, :ui_metrics_tracking, [])
262262
end
263+
264+
@impl true
265+
def credential_transfer_token_validity_in_days do
266+
2
267+
end
263268
end
264269

265270
@callback apollo(key :: atom() | nil) :: map()
@@ -300,6 +305,7 @@ defmodule Lightning.Config do
300305
@callback worker_secret() :: binary() | nil
301306
@callback worker_token_signer() :: Joken.Signer.t()
302307
@callback adaptor_registry() :: Keyword.t()
308+
@callback credential_transfer_token_validity_in_days() :: integer()
303309

304310
@doc """
305311
Returns the configuration for the `Lightning.AdaptorRegistry` service
@@ -474,6 +480,10 @@ defmodule Lightning.Config do
474480
impl().ui_metrics_tracking_enabled?()
475481
end
476482

483+
def credential_transfer_token_validity_in_days do
484+
impl().credential_transfer_token_validity_in_days()
485+
end
486+
477487
defp impl do
478488
Application.get_env(:lightning, __MODULE__, API)
479489
end

0 commit comments

Comments
 (0)