mirror of
https://github.com/discourse/discourse.git
synced 2026-08-11 01:06:53 +08:00
Previously, the only passwordless way to log in by email was a one-click magic link, enabled with `enable_local_logins_via_email`. This change adds `enable_local_logins_via_code`, which switches that flow to send a short one-time code instead of a link: when enabled, the login page offers "Email me a one-time login code" in place of the link option, and entering the emailed code logs the user in (including through any configured second factor). A code only ever logs an *existing* user in, and the request endpoint responds identically whether or not the email matches an account, so it can't be used to probe for accounts. Signing up with a one-time code builds on this and is in #40909. <details> <summary>Screenshots</summary> **Desktop** | Step | Foundation · Light | Foundation · Dark | Horizon · Light | Horizon · Dark | |---|---|---|---|---| | Password form (default) | <img width="1400" height="1200" alt="desktop-foundation-light-code-login-password-form" src="https://github.com/user-attachments/assets/aa6758f0-9402-4229-8ad8-962d9773b7bd" /> | <img width="1400" height="1200" alt="desktop-foundation-dark-code-login-password-form" src="https://github.com/user-attachments/assets/e4283e80-5973-48ba-8b24-e4a154fd04ea" /> | <img width="1400" height="1200" alt="desktop-horizon-light-code-login-password-form" src="https://github.com/user-attachments/assets/8b58915c-f040-40cf-aa28-50e763032ba1" /> | <img width="1400" height="1200" alt="desktop-horizon-dark-code-login-password-form" src="https://github.com/user-attachments/assets/4de1c283-1021-4ac1-b4e1-9e5e14a1a883" /> | | Email entry | <img width="1400" height="1200" alt="desktop-foundation-light-code-login-email-step" src="https://github.com/user-attachments/assets/9ecd42cd-b7a4-4eaa-b7a4-f3de2418d7df" /> | <img width="1400" height="1200" alt="desktop-foundation-dark-code-login-email-step" src="https://github.com/user-attachments/assets/6e2d106c-6fdd-498b-a470-85b5dafab2c7" /> | <img width="1400" height="1200" alt="desktop-horizon-light-code-login-email-step" src="https://github.com/user-attachments/assets/adfc7f9b-2651-43e1-a2ab-b37d1ff3e34f" /> | <img width="1400" height="1200" alt="desktop-horizon-dark-code-login-email-step" src="https://github.com/user-attachments/assets/4e6872e1-ce77-462d-a04f-2c81fa5e61ed" /> | | Code entry | <img width="1400" height="1200" alt="desktop-foundation-light-code-login-code-step" src="https://github.com/user-attachments/assets/b9b53768-e095-489a-bb62-d23e8b018d28" /> | <img width="1400" height="1200" alt="desktop-foundation-dark-code-login-code-step" src="https://github.com/user-attachments/assets/933e41fb-ea1b-434e-87b7-ffca6d651628" /> | <img width="1400" height="1200" alt="desktop-horizon-light-code-login-code-step" src="https://github.com/user-attachments/assets/936967cf-b657-4d9f-80e2-c44226b681c3" /> | <img width="1400" height="1200" alt="desktop-horizon-dark-code-login-code-step" src="https://github.com/user-attachments/assets/2bf4530c-e6ba-4561-963e-36a494288c3c" /> | | Invalid code | <img width="1400" height="1200" alt="desktop-foundation-light-code-login-wrong-code" src="https://github.com/user-attachments/assets/ddaf1ae4-66ec-499b-859c-c169ce544f3d" /> | <img width="1400" height="1200" alt="desktop-foundation-dark-code-login-wrong-code" src="https://github.com/user-attachments/assets/f6a57236-f498-4a98-ac80-aee3f7a1e1a5" /> | <img width="1400" height="1200" alt="desktop-horizon-light-code-login-wrong-code" src="https://github.com/user-attachments/assets/e8622433-d693-4faf-b2aa-f71c910c3b52" /> | <img width="1400" height="1200" alt="desktop-horizon-dark-code-login-wrong-code" src="https://github.com/user-attachments/assets/0221c9ba-5003-4160-9ef9-281045f2f318" /> | **Mobile** | Step | Foundation · Light | Foundation · Dark | Horizon · Light | Horizon · Dark | |---|---|---|---|---| | Password form (default) | <img width="1170" height="3600" alt="mobile-foundation-light-code-login-password-form" src="https://github.com/user-attachments/assets/6e25f7b4-915f-4f55-8b27-9f51ec4ec1ea" /> | <img width="1170" height="3600" alt="mobile-foundation-dark-code-login-password-form" src="https://github.com/user-attachments/assets/0e1b0009-8b70-454f-8167-01c7a8ce0664" /> | <img width="1170" height="3600" alt="mobile-horizon-light-code-login-password-form" src="https://github.com/user-attachments/assets/79949635-a8f5-4fc5-aa2f-22edb77f7b2d" /> | <img width="1170" height="3600" alt="mobile-horizon-dark-code-login-password-form" src="https://github.com/user-attachments/assets/1f7e962d-5e0b-4e22-bb54-0b27b870696f" /> | | Email entry | <img width="1170" height="3600" alt="mobile-foundation-light-code-login-email-step" src="https://github.com/user-attachments/assets/5577aad2-dd30-4424-af7a-af5f33458c8c" /> | <img width="1170" height="3600" alt="mobile-foundation-dark-code-login-email-step" src="https://github.com/user-attachments/assets/571b5bf2-0b07-46ec-ba3a-b80bc1078643" /> | <img width="1170" height="3600" alt="mobile-horizon-light-code-login-email-step" src="https://github.com/user-attachments/assets/3beb6702-e7df-434b-98d6-23203c6bee98" /> | <img width="1170" height="3600" alt="mobile-horizon-dark-code-login-email-step" src="https://github.com/user-attachments/assets/17b33dc8-c133-4f07-8817-1f1f7c6cf8ff" /> | | Code entry | <img width="1170" height="3600" alt="mobile-foundation-light-code-login-code-step" src="https://github.com/user-attachments/assets/91beeec5-87eb-4169-a53d-53164dd7dd2a" /> | <img width="1170" height="3600" alt="mobile-foundation-dark-code-login-code-step" src="https://github.com/user-attachments/assets/8d8de095-7050-4006-bc72-6a82f7c17513" /> | <img width="1170" height="3600" alt="mobile-horizon-light-code-login-code-step" src="https://github.com/user-attachments/assets/f874a1c6-2e9f-4c0c-b500-850c44067489" /> | <img width="1170" height="3600" alt="mobile-horizon-dark-code-login-code-step" src="https://github.com/user-attachments/assets/4957797c-00e9-45b8-8504-8baba15cde06" /> | | Invalid code | <img width="1170" height="3600" alt="mobile-foundation-light-code-login-wrong-code" src="https://github.com/user-attachments/assets/76124aa6-fd77-467a-b1bb-60e85e354612" /> | <img width="1170" height="3600" alt="mobile-foundation-dark-code-login-wrong-code" src="https://github.com/user-attachments/assets/c0f8d7db-9a7e-4803-928c-91a504bf8391" /> | <img width="1170" height="3600" alt="mobile-horizon-light-code-login-wrong-code" src="https://github.com/user-attachments/assets/77b97702-8ee7-4244-906e-6fddb5e22cae" /> | <img width="1170" height="3600" alt="mobile-horizon-dark-code-login-wrong-code" src="https://github.com/user-attachments/assets/e034b400-6430-4441-9b2a-c0f1b0bb49c6" /> | </details>
242 lines
8.7 KiB
Ruby
Vendored
242 lines
8.7 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
class Admin::EmailTemplatesController < Admin::AdminController
|
|
def self.restricted_key?(key)
|
|
Admin::SiteTextsController::RESTRICTED_KEYS.any? { |k| k.start_with?("#{key}.") }
|
|
end
|
|
|
|
# To update the list of keys below, run the `list_email_templates_strings`
|
|
# rake task and replace the list below with the output from the rake task.
|
|
def self.email_keys
|
|
@email_keys ||=
|
|
%w[
|
|
admin_confirmation_mailer
|
|
custom_invite_forum_mailer
|
|
custom_invite_mailer
|
|
download_backup_mailer
|
|
email_login_code_mailer
|
|
invite_forum_mailer
|
|
invite_mailer
|
|
invite_password_instructions
|
|
new_version_mailer
|
|
new_version_mailer_with_notes
|
|
system_messages.backup_failed
|
|
system_messages.backup_succeeded
|
|
system_messages.bulk_invite_failed
|
|
system_messages.bulk_invite_succeeded
|
|
system_messages.csv_export_failed
|
|
system_messages.csv_export_succeeded
|
|
system_messages.download_remote_images_disabled
|
|
system_messages.email_error_notification
|
|
system_messages.email_reject_attachment
|
|
system_messages.email_reject_auto_generated
|
|
system_messages.email_reject_bad_destination_address
|
|
system_messages.email_reject_empty
|
|
system_messages.email_reject_inactive_user
|
|
system_messages.email_reject_insufficient_trust_level
|
|
system_messages.email_reject_invalid_access
|
|
system_messages.email_reject_invalid_post
|
|
system_messages.email_reject_invalid_post_action
|
|
system_messages.email_reject_invalid_post_specified
|
|
system_messages.email_reject_not_allowed_email
|
|
system_messages.email_reject_old_destination
|
|
system_messages.email_reject_parsing
|
|
system_messages.email_reject_post_too_short
|
|
system_messages.email_reject_reply_key
|
|
system_messages.email_reject_reply_not_allowed
|
|
system_messages.email_reject_reply_to_digest
|
|
system_messages.email_reject_reply_user_not_matching
|
|
system_messages.email_reject_screened_email
|
|
system_messages.email_reject_silenced_user
|
|
system_messages.email_reject_strangers_not_allowed
|
|
system_messages.email_reject_too_many_recipients
|
|
system_messages.email_reject_topic_closed
|
|
system_messages.email_reject_topic_not_found
|
|
system_messages.email_reject_unrecognized_error
|
|
system_messages.email_reject_user_not_found
|
|
system_messages.email_revoked
|
|
system_messages.flags_agreed_and_post_deleted
|
|
system_messages.flags_agreed_and_post_deleted_for_responders
|
|
system_messages.flags_disagreed
|
|
system_messages.ignored_users_summary
|
|
system_messages.new_user_of_the_month
|
|
system_messages.pending_users_reminder
|
|
system_messages.post_hidden
|
|
system_messages.post_hidden_again
|
|
system_messages.queued_by_staff
|
|
system_messages.queued_posts_reminder
|
|
system_messages.restore_failed
|
|
system_messages.restore_succeeded
|
|
system_messages.reviewable_queued_post_revise_and_reject
|
|
system_messages.reviewable_queued_post_revise_and_reject_new_topic
|
|
system_messages.reviewables_reminder
|
|
system_messages.silenced_by_staff
|
|
system_messages.spam_post_blocked
|
|
system_messages.tl2_promotion_message
|
|
system_messages.too_many_spam_flags
|
|
system_messages.too_many_tl3_flags
|
|
system_messages.unsilenced
|
|
system_messages.user_added_to_group_as_member
|
|
system_messages.user_added_to_group_as_owner
|
|
system_messages.user_automatically_silenced
|
|
system_messages.user_automatically_silenced_with_reason
|
|
system_messages.user_posts_deleted
|
|
system_messages.welcome_invite
|
|
system_messages.welcome_staff
|
|
system_messages.welcome_tl1_user
|
|
system_messages.welcome_user
|
|
test_mailer
|
|
unsubscribe_mailer
|
|
user_notifications.account_created
|
|
user_notifications.account_deleted
|
|
user_notifications.account_exists
|
|
user_notifications.account_second_factor_disabled
|
|
user_notifications.account_silenced
|
|
user_notifications.account_silenced_forever
|
|
user_notifications.account_suspended
|
|
user_notifications.account_suspended_forever
|
|
user_notifications.activation_reminder
|
|
user_notifications.admin_login
|
|
user_notifications.confirm_new_email
|
|
user_notifications.confirm_new_email_via_admin
|
|
user_notifications.email_login
|
|
user_notifications.forgot_password
|
|
user_notifications.notify_old_email
|
|
user_notifications.notify_old_email_add
|
|
user_notifications.post_approved
|
|
user_notifications.set_password
|
|
user_notifications.signup
|
|
user_notifications.signup_after_approval
|
|
user_notifications.signup_after_reject
|
|
user_notifications.suspicious_login
|
|
user_notifications.user_group_mentioned
|
|
user_notifications.user_group_mentioned_pm
|
|
user_notifications.user_group_mentioned_pm_group
|
|
user_notifications.user_invited_to_private_message_pm
|
|
user_notifications.user_invited_to_private_message_pm_group
|
|
user_notifications.user_invited_to_private_message_pm_staged
|
|
user_notifications.user_invited_to_topic
|
|
user_notifications.user_linked
|
|
user_notifications.user_mentioned
|
|
user_notifications.user_mentioned_pm
|
|
user_notifications.user_posted
|
|
user_notifications.user_posted_pm
|
|
user_notifications.user_posted_pm_staged
|
|
user_notifications.user_quoted
|
|
user_notifications.user_replied
|
|
user_notifications.user_replied_pm
|
|
user_notifications.user_watching_category_or_tag
|
|
user_notifications.user_watching_first_post
|
|
].reject { |key| restricted_key?(key) }
|
|
|
|
DiscoursePluginRegistry.apply_modifier(:email_template_keys, @email_keys)
|
|
end
|
|
|
|
def show
|
|
end
|
|
|
|
def update
|
|
et = params[:email_template]
|
|
key = params[:id]
|
|
raise Discourse::NotFound if self.class.email_keys.exclude?(params[:id])
|
|
|
|
subject_result = update_key("#{key}.subject_template", et[:subject])
|
|
body_result = update_key("#{key}.text_body_template", et[:body])
|
|
|
|
error_messages = []
|
|
if subject_result[:error_messages].present?
|
|
error_messages << format_error_message(subject_result, "subject")
|
|
end
|
|
if body_result[:error_messages].present?
|
|
error_messages << format_error_message(body_result, "body")
|
|
end
|
|
|
|
if error_messages.blank?
|
|
log_site_text_change(subject_result)
|
|
log_site_text_change(body_result)
|
|
|
|
render_serialized(
|
|
key,
|
|
AdminEmailTemplateSerializer,
|
|
root: "email_template",
|
|
rest_serializer: true,
|
|
)
|
|
else
|
|
TranslationOverride.upsert!(
|
|
I18n.locale,
|
|
"#{key}.subject_template",
|
|
subject_result[:old_value],
|
|
)
|
|
TranslationOverride.upsert!(I18n.locale, "#{key}.text_body_template", body_result[:old_value])
|
|
|
|
render_json_error(error_messages)
|
|
end
|
|
end
|
|
|
|
def revert
|
|
key = params[:id]
|
|
raise Discourse::NotFound if self.class.email_keys.exclude?(params[:id])
|
|
|
|
revert_and_log("#{key}.subject_template", "#{key}.text_body_template")
|
|
render_serialized(
|
|
key,
|
|
AdminEmailTemplateSerializer,
|
|
root: "email_template",
|
|
rest_serializer: true,
|
|
)
|
|
end
|
|
|
|
def index
|
|
render_serialized(
|
|
self.class.email_keys,
|
|
AdminEmailTemplateSerializer,
|
|
root: "email_templates",
|
|
rest_serializer: true,
|
|
overridden_keys:,
|
|
)
|
|
end
|
|
|
|
private
|
|
|
|
def update_key(key, value)
|
|
old_value = I18n.t(key)
|
|
|
|
unless old_value.is_a?(Hash)
|
|
translation_override = TranslationOverride.upsert!(I18n.locale, key, value)
|
|
end
|
|
|
|
{ key:, old_value:, error_messages: translation_override&.errors&.full_messages }
|
|
end
|
|
|
|
def revert_and_log(*keys)
|
|
old_values = {}
|
|
keys.each { |key| old_values[key] = I18n.t(key) }
|
|
|
|
TranslationOverride.revert!(I18n.locale, keys)
|
|
|
|
keys.each do |key|
|
|
old_value = old_values[key]
|
|
new_value = I18n.t(key)
|
|
StaffActionLogger.new(current_user).log_site_text_change(key, new_value, old_value)
|
|
end
|
|
end
|
|
|
|
def log_site_text_change(update_result)
|
|
new_value = I18n.t(update_result[:key])
|
|
StaffActionLogger.new(current_user).log_site_text_change(
|
|
update_result[:key],
|
|
new_value,
|
|
update_result[:old_value],
|
|
)
|
|
end
|
|
|
|
def format_error_message(update_result, attribute_key)
|
|
attribute = I18n.t("admin_js.admin.customize.email_templates.#{attribute_key}")
|
|
message = update_result[:error_messages].join("<br>")
|
|
I18n.t("errors.format_with_full_message", attribute:, message:)
|
|
end
|
|
|
|
def overridden_keys
|
|
TranslationOverride.where(locale: I18n.locale).pluck(:translation_key)
|
|
end
|
|
end
|