feat: configure SMS OTP template for 2FA (#34585)

* feat: configure SMS OTP template for 2FA

* fix: jinja tags

* refactor: better func name
This commit is contained in:
Hussain Nagaria 2025-11-04 18:02:57 +05:30 committed by GitHub
parent f9e51b7abf
commit 1774021901
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 42 additions and 2 deletions

View file

@ -51,12 +51,15 @@
"column_break_34",
"allow_login_after_fail",
"two_factor_authentication",
"column_break_odhl",
"enable_two_factor_auth",
"bypass_2fa_for_retricted_ip_users",
"bypass_restrict_ip_check_if_2fa_enabled",
"column_break_bzfr",
"two_factor_method",
"lifespan_qrcode_image",
"otp_issuer_name",
"otp_sms_template",
"password_tab",
"password_settings",
"logout_on_password_reset",
@ -752,12 +755,27 @@
"fieldtype": "Select",
"label": "Show External Link Warning",
"options": "Never\nAsk\nAlways"
},
{
"fieldname": "column_break_odhl",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.enable_two_factor_auth==1 && doc.two_factor_method==\"SMS\"",
"description": "OTP placeholder should be defined as <code>{{ otp }}</code> ",
"fieldname": "otp_sms_template",
"fieldtype": "Small Text",
"label": "OTP SMS Template"
},
{
"fieldname": "column_break_bzfr",
"fieldtype": "Column Break"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
"modified": "2025-09-24 16:04:02.016562",
"modified": "2025-11-04 16:47:54.230874",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",

View file

@ -89,6 +89,7 @@ class SystemSettings(Document):
"#,###",
]
otp_issuer_name: DF.Data | None
otp_sms_template: DF.SmallText | None
password_reset_limit: DF.Int
rate_limit_email_link_login: DF.Int
reset_password_link_expiry_duration: DF.Duration | None
@ -145,6 +146,7 @@ class SystemSettings(Document):
self.validate_user_pass_login()
self.validate_backup_limit()
self.validate_file_extensions()
self.validate_otp_sms_template()
if not self.link_field_results_limit:
self.link_field_results_limit = 10
@ -156,6 +158,17 @@ class SystemSettings(Document):
_("{0} can not be more than {1}").format(label, 50), alert=True, indicator="yellow"
)
def validate_otp_sms_template(self):
if not self.enable_two_factor_auth or self.two_factor_method != "SMS" or not self.otp_sms_template:
return
if "{{otp}}" not in self.otp_sms_template.replace(" ", ""):
frappe.throw(
_("OTP SMS Template must contain <code>{0}</code> placeholder to insert the OTP.").format(
"{{otp}}"
)
)
def validate_user_pass_login(self):
if not self.disable_user_pass_login:
return

View file

@ -320,7 +320,8 @@ def send_token_via_sms(otpsecret, token=None, phone_no=None):
return False
hotp = pyotp.HOTP(otpsecret)
args = {ss.message_parameter: f"Your verification code is {hotp.at(int(token))}"}
otp = hotp.at(int(token))
args = {ss.message_parameter: get_rendered_otp_message(otp)}
for d in ss.get("parameters"):
args[d.parameter] = d.value
@ -341,6 +342,14 @@ def send_token_via_sms(otpsecret, token=None, phone_no=None):
return True
def get_rendered_otp_message(otp: str) -> str:
default_template = "Your verification code is {{otp}}"
custom_template = frappe.get_system_settings("otp_sms_template")
template = custom_template or default_template
return frappe.render_template(template, {"otp": otp})
def send_token_via_email(user, token, otp_secret, otp_issuer, subject=None, message=None):
"""Send token to user as email."""
user_email = frappe.db.get_value("User", user, "email")