Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/admin/auth.rst
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,7 @@
through the authentication process. This needs to be configured with some
identity providers:

* `How to Send a Custom RelayState with Okta`_

Check warning on line 573 in docs/admin/auth.rst

View workflow job for this annotation

GitHub Actions / Linkcheck

https://support.okta.com/help/s/article/How-to-send-a-custom-relaystate-to-application-through-idp-initiated-authentication-urls to https://support.okta.com/help/s/article/How-to-send-a-custom-relaystate-to-application-through-idp-initiated-authentication-urls?language=en_US

.. _How to Send a Custom RelayState with Okta: https://support.okta.com/help/s/article/How-to-send-a-custom-relaystate-to-application-through-idp-initiated-authentication-urls

Expand Down Expand Up @@ -638,6 +638,9 @@
# Email is required for Weblate (used in VCS commits)
"email": "mail",
}
# Optional: route "Forgot your password?" to any service of choice
PASSWORD_RESET_URL = "https://id.example.net/password-reset/"


# Hide the registration form
REGISTRATION_OPEN = False
Expand Down
12 changes: 12 additions & 0 deletions docs/admin/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,18 @@ Insert additional markup into the HTML header. Can be used for verification of s

No sanitization is performed on the string. It is inserted as-is into the HTML header.

.. setting:: PASSWORD_RESET_URL

PASSWORD_RESET_URL
------------------

URL for password reset when authentication is handled by an external identity provider
such as LDAP, SAML, or OAuth.

When set, :guilabel:`Forgot your password?` on the sign-in page links to this URL
instead of Weblate's built-in password reset page.

Comment on lines +839 to +849
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new PASSWORD_RESET_URL setting documentation is missing the usual .. versionadded:: directive used for other settings in this file. Also, docs/admin/config.rst appears to keep settings sections roughly ordered; consider placing this entry near the other PASSWORD_* settings to keep the reference easy to navigate.

Copilot uses AI. Check for mistakes.
Comment on lines +844 to +849
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces user-visible behavior/a new setting; the repo’s changelog (docs/changes.rst) typically gets an entry for such changes. Please add a short note under the appropriate rubric so administrators notice PASSWORD_RESET_URL in release notes.

Copilot uses AI. Check for mistakes.

.. setting:: GET_HELP_URL

GET_HELP_URL
Expand Down
4 changes: 4 additions & 0 deletions docs/admin/install/docker.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1865,6 +1865,10 @@ Site integration

Configures :setting:`PRIVACY_URL`.

.. envvar:: WEBLATE_PASSWORD_RESET_URL

Configures :setting:`PASSWORD_RESET_URL`.

Collecting error reports and monitoring performance
+++++++++++++++++++++++++++++++++++++++++++++++++++

Expand Down
24 changes: 24 additions & 0 deletions weblate/accounts/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,30 @@ def test_login_anonymous(self) -> None:
response, "This username/password combination was not found."
)

@override_settings(
AUTHENTICATION_BACKENDS=(
"django.contrib.auth.backends.ModelBackend",
"weblate.accounts.auth.WeblateUserBackend",
),
REGISTRATION_OPEN=False,
PASSWORD_RESET_URL="https://id.example.net/password-reset",
)
def test_login_password_reset_url(self) -> None:
response = self.client.get(reverse("login"))
self.assertContains(response, 'href="https://id.example.net/password-reset"')

@override_settings(
AUTHENTICATION_BACKENDS=(
"django.contrib.auth.backends.ModelBackend",
"weblate.accounts.auth.WeblateUserBackend",
),
REGISTRATION_OPEN=False,
PASSWORD_RESET_URL=None,
)
Comment on lines +292 to +311
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests override AUTHENTICATION_BACKENDS using override_settings, but social_core caches loaded backends globally; other tests in the repo use social_core_override_settings to clear this cache when changing AUTHENTICATION_BACKENDS. These new tests can become order-dependent/flaky unless they also clear social_core.backends.utils.BACKENDSCACHE (e.g., switch to social_core_override_settings or reset the cache in setup/teardown).

Copilot uses AI. Check for mistakes.
def test_login_without_configured_password_reset_url(self) -> None:
response = self.client.get(reverse("login"))
self.assertNotContains(response, reverse("password_reset"))

Comment on lines +300 to +315
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current tests cover (a) external PASSWORD_RESET_URL displayed when e-mail auth is disabled and (b) no reset link when neither internal nor external reset is available. The PR description also promises a fallback to the internal password_reset when PASSWORD_RESET_URL is not set, but there is no test asserting that behavior when social_core.backends.email.EmailAuth is enabled. Please add a test that verifies the login page includes the internal reset URL when e-mail auth is available and PASSWORD_RESET_URL is unset.

Copilot generated this review using guidance from repository custom instructions.
@override_settings(RATELIMIT_ATTEMPTS=20, AUTH_LOCK_ATTEMPTS=5)
def test_login_ratelimit(self, login=False) -> None:
if login:
Expand Down
6 changes: 5 additions & 1 deletion weblate/accounts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@

# Populate scopes from the database
for subscription in user.subscription_set.select_related("project", "component"):
key = (

Check failure on line 367 in weblate/accounts/views.py

View workflow job for this annotation

GitHub Actions / mypy

Incompatible types in assignment (expression has type "tuple[int, int, int]", variable has type "tuple[NotificationScope, int, int]")
subscription.scope,
subscription.project_id or -1,
subscription.component_id or -1,
Expand Down Expand Up @@ -480,8 +480,8 @@
"new_backends": new_backends,
"has_email_auth": "email" in all_backends,
"auditlog": user.auditlog_set.order()[:20],
"totp_keys": user.totpdevice_set.all(),

Check failure on line 483 in weblate/accounts/views.py

View workflow job for this annotation

GitHub Actions / mypy

"User" has no attribute "totpdevice_set"
"webauthn_keys": user.webauthncredential_set.all(),

Check failure on line 484 in weblate/accounts/views.py

View workflow job for this annotation

GitHub Actions / mypy

"User" has no attribute "webauthncredential_set"
"recovery_keys_count": StaticToken.objects.filter(
device__user=user
).count(),
Expand Down Expand Up @@ -728,7 +728,7 @@

# Filter where project is active
user_translation_ids = set(
all_changes.content()

Check failure on line 731 in weblate/accounts/views.py

View workflow job for this annotation

GitHub Actions / mypy

"QuerySet[Change, Change]" has no attribute "content"
.filter(timestamp__gte=timezone.now() - timedelta(days=90))
.values_list("translation", flat=True)
)
Expand All @@ -743,7 +743,7 @@

context["page_profile"] = user.profile
# Last user activity
context["last_changes"] = all_changes.recent()

Check failure on line 746 in weblate/accounts/views.py

View workflow job for this annotation

GitHub Actions / mypy

"QuerySet[Change, Change]" has no attribute "recent"
context["last_changes_url"] = urlencode({"user": user.username})
context["page_user_translations"] = translation_prefetch_tasks(
prefetch_stats(user_translations)
Expand Down Expand Up @@ -898,8 +898,12 @@
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
auth_backends = get_auth_keys()
reset_url = getattr(settings, "PASSWORD_RESET_URL", None) or getattr(
settings, "EXTERNAL_PASSWORD_RESET_URL", None
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please include PASSWORD_RESET_URL in WeblateAccountsConf, so that it can be accessed directly from the settings.

context["login_backends"] = [x for x in sorted(auth_backends) if x != "email"]
context["can_reset"] = self.has_email_auth
context["can_reset"] = self.has_email_auth or bool(reset_url)
context["reset_url"] = reset_url
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be better to pass using the existing context processor in weblate/trans/context_processors.py.

# Show login form for e-mail login or any third-party Django auth backend such as LDAP
context["show_login_form"] = self.show_login_form
context["title"] = gettext("Sign in")
Expand Down Expand Up @@ -1318,7 +1322,7 @@
else:
user = get_object_or_404(User, username=self.kwargs["user"])
return (
Suggestion.objects.filter_access(self.request.user)

Check failure on line 1325 in weblate/accounts/views.py

View workflow job for this annotation

GitHub Actions / mypy

Argument 1 to "filter_access" of "SuggestionQuerySet" has incompatible type "User | AnonymousUser"; expected "User"
.filter(user=user)
.order()
)
Expand Down Expand Up @@ -1362,7 +1366,7 @@
return redirect_profile("#account")

# Block removal of last verified email
verified = VerifiedEmail.objects.filter(social__user=request.user).exclude(

Check failure on line 1369 in weblate/accounts/views.py

View workflow job for this annotation

GitHub Actions / mypy

Incompatible type for lookup 'social_id': (got "str | None", expected "UserSocialAuth | int | None")
social__provider=backend, social_id=association_id
)
if not verified.exists():
Expand Down Expand Up @@ -1651,7 +1655,7 @@
search = form.cleaned_data.get("q", "")
if search:
users = users.search(
search, parser=form.fields["q"].parser, user=self.request.user

Check failure on line 1658 in weblate/accounts/views.py

View workflow job for this annotation

GitHub Actions / mypy

"Field" has no attribute "parser"
)
else:
users = users.order()
Expand Down Expand Up @@ -1746,7 +1750,7 @@
AuditLog.objects.create(
request.user, request, "twofactor-remove", device=key_name
)
messages.success(request, self.message_remove)

Check failure on line 1753 in weblate/accounts/views.py

View workflow job for this annotation

GitHub Actions / mypy

Argument 2 to "success" has incompatible type "_StrPromise"; expected "str"
elif "name" in request.POST:
obj.name = request.POST["name"]
obj.save(update_fields=["name"])
Expand Down Expand Up @@ -1842,7 +1846,7 @@
device=get_key_name(device),
)
if form.cleaned_data["remove_previous"]:
for old in user.totpdevice_set.exclude(pk=device.pk):

Check failure on line 1849 in weblate/accounts/views.py

View workflow job for this annotation

GitHub Actions / mypy

"User" has no attribute "totpdevice_set"
key_name = get_key_name(old)
old.delete()
AuditLog.objects.create(
Expand Down
4 changes: 3 additions & 1 deletion weblate/settings_docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1475,7 +1475,9 @@
STATUS_URL = get_env_str("WEBLATE_STATUS_URL")
LEGAL_URL = get_env_str("WEBLATE_LEGAL_URL")
PRIVACY_URL = get_env_str("WEBLATE_PRIVACY_URL")

PASSWORD_RESET_URL = get_env_str(
"WEBLATE_PASSWORD_RESET_URL", get_env_str("WEBLATE_EXTERNAL_PASSWORD_RESET_URL")
)
# Third party services integration
MATOMO_SITE_ID = get_env_str("WEBLATE_MATOMO_SITE_ID")
MATOMO_URL = get_env_str("WEBLATE_MATOMO_URL")
Expand Down
1 change: 1 addition & 0 deletions weblate/settings_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -939,3 +939,4 @@
GOOGLE_ANALYTICS_ID = None
SENTRY_DSN = None
SENTRY_ENVIRONMENT = SITE_DOMAIN
PASSWORD_RESET_URL = None
4 changes: 3 additions & 1 deletion weblate/templates/accounts/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ <h5 class="login-label">{% translate "Sign in with:" %}</h5>
<ul class="login-links">
{% if can_reset %}
<li>
<a href="{% url 'password_reset' %}">{% translate "Forgot your password?" %}</a>
<a href="{% if reset_url %}{{ reset_url }}{% else %}{% url 'password_reset' %}{% endif %}">
{% translate "Forgot your password?" %}
</a>
</li>
{% endif %}

Expand Down
Loading