mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-05-12 04:11:49 +00:00
Email verification
This commit is contained in:
parent
7836f35605
commit
69988b98d0
45 changed files with 2033 additions and 388 deletions
backend
src/baserow
api
contrib
core
apps.py
auth_provider
emails.pyhandler.pylocale/en/LC_MESSAGES
migrations
models.pytemplates/baserow/core/user
user
tests/baserow
api
core
changelog/entries/unreleased/feature
docs/patterns
enterprise/backend/src/baserow_enterprise/locale/en/LC_MESSAGES
premium/backend/src/baserow_premium/locale/en/LC_MESSAGES
web-frontend/modules/core
|
@ -38,6 +38,7 @@ class SettingsSerializer(serializers.ModelSerializer):
|
|||
"track_workspace_usage",
|
||||
"show_baserow_help_request",
|
||||
"co_branding_logo",
|
||||
"email_verification",
|
||||
)
|
||||
extra_kwargs = {
|
||||
"allow_new_signups": {"required": False},
|
||||
|
@ -47,8 +48,29 @@ class SettingsSerializer(serializers.ModelSerializer):
|
|||
"account_deletion_grace_delay": {"required": False},
|
||||
"track_workspace_usage": {"required": False},
|
||||
"show_baserow_help_request": {"required": False},
|
||||
"email_verification": {"required": False},
|
||||
}
|
||||
|
||||
def to_representation(self, instance):
|
||||
representation = super().to_representation(instance)
|
||||
# TODO Remove in a future release once email_verification is null=False
|
||||
if representation["email_verification"] is None:
|
||||
representation[
|
||||
"email_verification"
|
||||
] = Settings.EmailVerificationOptions.NO_VERIFICATION
|
||||
|
||||
return representation
|
||||
|
||||
def to_internal_value(self, data):
|
||||
# TODO Remove in a future release once email_verification is null=False
|
||||
if "email_verification" in data and data["email_verification"] is None:
|
||||
raise serializers.ValidationError(
|
||||
detail={"email_verification": ["'null' is not a valid choice."]},
|
||||
code="invalid_choice",
|
||||
)
|
||||
|
||||
return super().to_internal_value(data)
|
||||
|
||||
|
||||
class InstanceIdSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
|
|
|
@ -20,6 +20,17 @@ ERROR_CLIENT_SESSION_ID_HEADER_NOT_SET = (
|
|||
)
|
||||
ERROR_DISABLED_RESET_PASSWORD = "ERROR_DISABLED_RESET_PASSWORD" # nosec
|
||||
|
||||
ERROR_EMAIL_ALREADY_VERIFIED = (
|
||||
"ERROR_EMAIL_ALREADY_VERIFIED",
|
||||
HTTP_400_BAD_REQUEST,
|
||||
"The user's email is verified already.",
|
||||
)
|
||||
ERROR_INVALID_VERIFICATION_TOKEN = (
|
||||
"ERROR_INVALID_VERIFICATION_TOKEN",
|
||||
HTTP_400_BAD_REQUEST,
|
||||
"A valid verification token is required.",
|
||||
)
|
||||
|
||||
ERROR_UNDO_REDO_LOCK_CONFLICT = (
|
||||
"ERROR_UNDO_REDO_LOCK_CONFLICT",
|
||||
HTTP_409_CONFLICT,
|
||||
|
@ -57,6 +68,12 @@ ERROR_AUTH_PROVIDER_DISABLED = (
|
|||
"Authentication provider is disabled.",
|
||||
)
|
||||
|
||||
ERROR_EMAIL_VERIFICATION_REQUIRED = (
|
||||
"ERROR_EMAIL_VERIFICATION_REQUIRED",
|
||||
HTTP_401_UNAUTHORIZED,
|
||||
"Email address has to be verified first.",
|
||||
)
|
||||
|
||||
ERROR_REFRESH_TOKEN_ALREADY_BLACKLISTED = (
|
||||
"ERROR_REFRESH_TOKEN_ALREADY_BLACKLISTED",
|
||||
HTTP_400_BAD_REQUEST,
|
||||
|
|
|
@ -23,11 +23,16 @@ from baserow.api.workspaces.invitations.serializers import (
|
|||
UserWorkspaceInvitationSerializer,
|
||||
)
|
||||
from baserow.core.action.registries import action_type_registry
|
||||
from baserow.core.auth_provider.exceptions import AuthProviderDisabled
|
||||
from baserow.core.auth_provider.exceptions import (
|
||||
AuthProviderDisabled,
|
||||
EmailVerificationRequired,
|
||||
)
|
||||
from baserow.core.auth_provider.handler import PasswordProviderHandler
|
||||
from baserow.core.models import Template, UserProfile
|
||||
from baserow.core.handler import CoreHandler
|
||||
from baserow.core.models import Settings, Template, UserProfile
|
||||
from baserow.core.user.actions import SignInUserActionType
|
||||
from baserow.core.user.exceptions import DeactivatedUserException
|
||||
from baserow.core.user.handler import UserHandler
|
||||
from baserow.core.user.utils import (
|
||||
generate_session_tokens_for_user,
|
||||
normalize_email_address,
|
||||
|
@ -72,6 +77,10 @@ class UserSerializer(serializers.ModelSerializer):
|
|||
help_text="The maximum frequency at which the user wants to "
|
||||
"receive email notifications.",
|
||||
)
|
||||
email_verified = serializers.BooleanField(
|
||||
source="profile.email_verified",
|
||||
help_text="Shows whether the user's email has been verified.",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
|
@ -83,11 +92,13 @@ class UserSerializer(serializers.ModelSerializer):
|
|||
"id",
|
||||
"language",
|
||||
"email_notification_frequency",
|
||||
"email_verified",
|
||||
)
|
||||
extra_kwargs = {
|
||||
"password": {"write_only": True},
|
||||
"is_staff": {"read_only": True},
|
||||
"id": {"read_only": True},
|
||||
"email_verified": {"read_only": True},
|
||||
}
|
||||
|
||||
|
||||
|
@ -205,6 +216,14 @@ class ChangePasswordBodyValidationSerializer(serializers.Serializer):
|
|||
new_password = serializers.CharField(validators=[password_validation])
|
||||
|
||||
|
||||
class VerifyEmailAddressSerializer(serializers.Serializer):
|
||||
token = serializers.CharField()
|
||||
|
||||
|
||||
class SendVerifyEmailAddressSerializer(serializers.Serializer):
|
||||
email = serializers.EmailField()
|
||||
|
||||
|
||||
class NormalizedEmailField(serializers.EmailField):
|
||||
def to_internal_value(self, data):
|
||||
data = super().to_internal_value(data)
|
||||
|
@ -267,7 +286,19 @@ class TokenObtainPairWithUserSerializer(TokenObtainPairSerializer):
|
|||
if not user.is_active:
|
||||
raise DeactivatedUserException()
|
||||
|
||||
data = generate_session_tokens_for_user(user, include_refresh_token=True)
|
||||
settings = CoreHandler().get_settings()
|
||||
if (
|
||||
settings.email_verification == Settings.EmailVerificationOptions.ENFORCED
|
||||
and not user.profile.email_verified
|
||||
):
|
||||
UserHandler().send_email_pending_verification(user)
|
||||
raise EmailVerificationRequired()
|
||||
|
||||
data = generate_session_tokens_for_user(
|
||||
user,
|
||||
include_refresh_token=True,
|
||||
verified_email_claim=Settings.EmailVerificationOptions.ENFORCED,
|
||||
)
|
||||
data.update(**get_all_user_data_serialized(user, self.context["request"]))
|
||||
|
||||
set_user_session_data_from_request(user, self.context["request"])
|
||||
|
@ -294,6 +325,17 @@ class TokenRefreshWithUserSerializer(TokenRefreshSerializer):
|
|||
user = get_user_from_token(
|
||||
attrs["refresh"], RefreshToken, check_if_refresh_token_is_blacklisted=True
|
||||
)
|
||||
|
||||
token = RefreshToken(attrs["refresh"])
|
||||
settings = CoreHandler().get_settings()
|
||||
if (
|
||||
settings.email_verification == Settings.EmailVerificationOptions.ENFORCED
|
||||
and not user.profile.email_verified
|
||||
and token.get("verified_email_claim")
|
||||
== Settings.EmailVerificationOptions.ENFORCED
|
||||
):
|
||||
raise EmailVerificationRequired()
|
||||
|
||||
data = generate_session_tokens_for_user(user)
|
||||
data.update(**get_all_user_data_serialized(user, self.context["request"]))
|
||||
token_refreshes_counter.add(1)
|
||||
|
@ -319,6 +361,17 @@ class TokenVerifyWithUserSerializer(TokenVerifySerializer):
|
|||
token_class=RefreshToken,
|
||||
check_if_refresh_token_is_blacklisted=True,
|
||||
)
|
||||
|
||||
token = RefreshToken(refresh_token)
|
||||
settings = CoreHandler().get_settings()
|
||||
if (
|
||||
settings.email_verification == Settings.EmailVerificationOptions.ENFORCED
|
||||
and not user.profile.email_verified
|
||||
and token.get("verified_email_claim")
|
||||
== Settings.EmailVerificationOptions.ENFORCED
|
||||
):
|
||||
raise EmailVerificationRequired()
|
||||
|
||||
return get_all_user_data_serialized(user, self.context["request"])
|
||||
|
||||
|
||||
|
|
|
@ -11,8 +11,10 @@ from .views import (
|
|||
ResetPasswordView,
|
||||
ScheduleAccountDeletionView,
|
||||
SendResetPasswordEmailView,
|
||||
SendVerifyEmailView,
|
||||
UndoView,
|
||||
UserView,
|
||||
VerifyEmailAddressView,
|
||||
VerifyJSONWebToken,
|
||||
)
|
||||
|
||||
|
@ -40,6 +42,10 @@ urlpatterns = [
|
|||
re_path(
|
||||
r"^change-password/$", ChangePasswordView.as_view(), name="change_password"
|
||||
),
|
||||
re_path(
|
||||
r"^send-verify-email/$", SendVerifyEmailView.as_view(), name="send_verify_email"
|
||||
),
|
||||
re_path(r"^verify-email/$", VerifyEmailAddressView.as_view(), name="verify_email"),
|
||||
re_path(r"^dashboard/$", DashboardView.as_view(), name="dashboard"),
|
||||
re_path(r"^undo/$", UndoView.as_view(), name="undo"),
|
||||
re_path(r"^redo/$", RedoView.as_view(), name="redo"),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from typing import List
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import transaction
|
||||
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
|
@ -42,7 +43,10 @@ from baserow.api.workspaces.invitations.errors import (
|
|||
)
|
||||
from baserow.core.action.handler import ActionHandler
|
||||
from baserow.core.action.registries import ActionScopeStr, action_type_registry
|
||||
from baserow.core.auth_provider.exceptions import AuthProviderDisabled
|
||||
from baserow.core.auth_provider.exceptions import (
|
||||
AuthProviderDisabled,
|
||||
EmailVerificationRequired,
|
||||
)
|
||||
from baserow.core.auth_provider.handler import PasswordProviderHandler
|
||||
from baserow.core.exceptions import (
|
||||
BaseURLHostnameNotAllowed,
|
||||
|
@ -50,19 +54,24 @@ from baserow.core.exceptions import (
|
|||
WorkspaceInvitationDoesNotExist,
|
||||
WorkspaceInvitationEmailMismatch,
|
||||
)
|
||||
from baserow.core.models import Template, WorkspaceInvitation
|
||||
from baserow.core.handler import CoreHandler
|
||||
from baserow.core.models import Settings, Template, WorkspaceInvitation
|
||||
from baserow.core.user.actions import (
|
||||
ChangeUserPasswordActionType,
|
||||
CreateUserActionType,
|
||||
ResetUserPasswordActionType,
|
||||
ScheduleUserDeletionActionType,
|
||||
SendResetUserPasswordActionType,
|
||||
SendVerifyEmailAddressActionType,
|
||||
UpdateUserActionType,
|
||||
VerifyEmailAddressActionType,
|
||||
)
|
||||
from baserow.core.user.exceptions import (
|
||||
DeactivatedUserException,
|
||||
DisabledSignupError,
|
||||
EmailAlreadyVerified,
|
||||
InvalidPassword,
|
||||
InvalidVerificationToken,
|
||||
RefreshTokenAlreadyBlacklisted,
|
||||
ResetPasswordDisabledError,
|
||||
UserAlreadyExist,
|
||||
|
@ -79,9 +88,12 @@ from .errors import (
|
|||
ERROR_DEACTIVATED_USER,
|
||||
ERROR_DISABLED_RESET_PASSWORD,
|
||||
ERROR_DISABLED_SIGNUP,
|
||||
ERROR_EMAIL_ALREADY_VERIFIED,
|
||||
ERROR_EMAIL_VERIFICATION_REQUIRED,
|
||||
ERROR_INVALID_CREDENTIALS,
|
||||
ERROR_INVALID_OLD_PASSWORD,
|
||||
ERROR_INVALID_REFRESH_TOKEN,
|
||||
ERROR_INVALID_VERIFICATION_TOKEN,
|
||||
ERROR_REFRESH_TOKEN_ALREADY_BLACKLISTED,
|
||||
ERROR_UNDO_REDO_LOCK_CONFLICT,
|
||||
ERROR_USER_IS_LAST_ADMIN,
|
||||
|
@ -100,13 +112,16 @@ from .serializers import (
|
|||
RegisterSerializer,
|
||||
ResetPasswordBodyValidationSerializer,
|
||||
SendResetPasswordEmailBodyValidationSerializer,
|
||||
SendVerifyEmailAddressSerializer,
|
||||
TokenBlacklistSerializer,
|
||||
TokenObtainPairWithUserSerializer,
|
||||
TokenRefreshWithUserSerializer,
|
||||
TokenVerifyWithUserSerializer,
|
||||
UserSerializer,
|
||||
VerifyEmailAddressSerializer,
|
||||
)
|
||||
|
||||
User = get_user_model()
|
||||
UndoRedoRequestSerializer = get_undo_request_serializer()
|
||||
|
||||
|
||||
|
@ -128,10 +143,14 @@ class ObtainJSONWebToken(TokenObtainPairView):
|
|||
),
|
||||
responses={
|
||||
200: create_user_response_schema,
|
||||
401: {
|
||||
"description": "An active user with the provided email and password "
|
||||
"could not be found."
|
||||
},
|
||||
401: get_error_schema(
|
||||
[
|
||||
"ERROR_INVALID_CREDENTIALS",
|
||||
"ERROR_DEACTIVATED_USER",
|
||||
"ERROR_AUTH_PROVIDER_DISABLED",
|
||||
"ERROR_EMAIL_VERIFICATION_REQUIRED",
|
||||
]
|
||||
),
|
||||
},
|
||||
auth=[],
|
||||
)
|
||||
|
@ -140,6 +159,7 @@ class ObtainJSONWebToken(TokenObtainPairView):
|
|||
AuthenticationFailed: ERROR_INVALID_CREDENTIALS,
|
||||
DeactivatedUserException: ERROR_DEACTIVATED_USER,
|
||||
AuthProviderDisabled: ERROR_AUTH_PROVIDER_DISABLED,
|
||||
EmailVerificationRequired: ERROR_EMAIL_VERIFICATION_REQUIRED,
|
||||
}
|
||||
)
|
||||
def post(self, *args, **kwargs):
|
||||
|
@ -158,11 +178,18 @@ class RefreshJSONWebToken(TokenRefreshView):
|
|||
),
|
||||
responses={
|
||||
200: authenticate_user_schema,
|
||||
401: {"description": "The JWT refresh token is invalid or expired."},
|
||||
401: get_error_schema(
|
||||
["ERROR_INVALID_REFRESH_TOKEN", "ERROR_EMAIL_VERIFICATION_REQUIRED"]
|
||||
),
|
||||
},
|
||||
auth=[],
|
||||
)
|
||||
@map_exceptions({InvalidToken: ERROR_INVALID_REFRESH_TOKEN})
|
||||
@map_exceptions(
|
||||
{
|
||||
InvalidToken: ERROR_INVALID_REFRESH_TOKEN,
|
||||
EmailVerificationRequired: ERROR_EMAIL_VERIFICATION_REQUIRED,
|
||||
}
|
||||
)
|
||||
def post(self, *args, **kwargs):
|
||||
return super().post(*args, **kwargs)
|
||||
|
||||
|
@ -179,11 +206,18 @@ class VerifyJSONWebToken(TokenVerifyView):
|
|||
),
|
||||
responses={
|
||||
200: verify_user_schema,
|
||||
401: {"description": "The JWT refresh token is invalid or expired."},
|
||||
401: get_error_schema(
|
||||
["ERROR_INVALID_REFRESH_TOKEN", "ERROR_EMAIL_VERIFICATION_REQUIRED"]
|
||||
),
|
||||
},
|
||||
auth=[],
|
||||
)
|
||||
@map_exceptions({InvalidToken: ERROR_INVALID_REFRESH_TOKEN})
|
||||
@map_exceptions(
|
||||
{
|
||||
InvalidToken: ERROR_INVALID_REFRESH_TOKEN,
|
||||
EmailVerificationRequired: ERROR_EMAIL_VERIFICATION_REQUIRED,
|
||||
}
|
||||
)
|
||||
def post(self, *args, **kwargs):
|
||||
return super().post(*args, **kwargs)
|
||||
|
||||
|
@ -287,15 +321,21 @@ class UserView(APIView):
|
|||
|
||||
response = {"user": UserSerializer(user).data}
|
||||
|
||||
if data["authenticate"]:
|
||||
response.update(
|
||||
{
|
||||
**generate_session_tokens_for_user(
|
||||
user, include_refresh_token=True
|
||||
),
|
||||
**user_data_registry.get_all_user_data(user, request),
|
||||
}
|
||||
)
|
||||
settings = CoreHandler().get_settings()
|
||||
|
||||
if (
|
||||
data["authenticate"]
|
||||
and settings.email_verification
|
||||
!= Settings.EmailVerificationOptions.ENFORCED
|
||||
):
|
||||
response |= {
|
||||
**generate_session_tokens_for_user(
|
||||
user,
|
||||
verified_email_claim=Settings.EmailVerificationOptions.ENFORCED,
|
||||
include_refresh_token=True,
|
||||
),
|
||||
**user_data_registry.get_all_user_data(user, request),
|
||||
}
|
||||
|
||||
return Response(response)
|
||||
|
||||
|
@ -471,6 +511,77 @@ class AccountView(APIView):
|
|||
return Response(AccountSerializer(user).data)
|
||||
|
||||
|
||||
class SendVerifyEmailView(APIView):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
@extend_schema(
|
||||
tags=["User"],
|
||||
operation_id="send_verify_email",
|
||||
description=(
|
||||
"Sends an email to the user "
|
||||
"with an email verification link "
|
||||
"if the user's email is not verified yet."
|
||||
),
|
||||
responses={
|
||||
204: None,
|
||||
},
|
||||
)
|
||||
@transaction.atomic
|
||||
@validate_body(SendVerifyEmailAddressSerializer)
|
||||
def post(self, request, data):
|
||||
"""
|
||||
Sends a verify email link to the user.
|
||||
"""
|
||||
|
||||
try:
|
||||
handler = UserHandler()
|
||||
user = handler.get_active_user(email=data["email"])
|
||||
action_type_registry.get(SendVerifyEmailAddressActionType.type).do(user)
|
||||
except (EmailAlreadyVerified, UserNotFound):
|
||||
# ignore as not to leak existing email addresses
|
||||
...
|
||||
return Response(status=204)
|
||||
|
||||
|
||||
class VerifyEmailAddressView(APIView):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
@extend_schema(
|
||||
tags=["User"],
|
||||
request=VerifyEmailAddressSerializer,
|
||||
operation_id="verify_email",
|
||||
description=(
|
||||
"Passing the correct verification token will "
|
||||
"confirm that the user's email address belongs to "
|
||||
"the user."
|
||||
),
|
||||
responses={
|
||||
204: None,
|
||||
400: get_error_schema(
|
||||
[
|
||||
"ERROR_INVALID_VERIFICATION_TOKEN",
|
||||
"ERROR_EMAIL_ALREADY_VERIFIED",
|
||||
]
|
||||
),
|
||||
},
|
||||
)
|
||||
@transaction.atomic
|
||||
@map_exceptions(
|
||||
{
|
||||
InvalidVerificationToken: ERROR_INVALID_VERIFICATION_TOKEN,
|
||||
EmailAlreadyVerified: ERROR_EMAIL_ALREADY_VERIFIED,
|
||||
}
|
||||
)
|
||||
@validate_body(VerifyEmailAddressSerializer)
|
||||
def post(self, request, data):
|
||||
"""
|
||||
Verifies that the user's email belong to the user.
|
||||
"""
|
||||
|
||||
action_type_registry.get(VerifyEmailAddressActionType.type).do(data["token"])
|
||||
return Response(status=204)
|
||||
|
||||
|
||||
class ScheduleAccountDeletionView(APIView):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-01-18 13:32+0000\n"
|
||||
"POT-Creation-Date: 2024-04-22 17:45+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -50,9 +50,9 @@ msgstr ""
|
|||
msgid "Data source"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/builder/elements/element_types.py:184
|
||||
#: src/baserow/contrib/builder/elements/element_types.py:189
|
||||
#: src/baserow/contrib/builder/elements/element_types.py:194
|
||||
#: src/baserow/contrib/builder/elements/mixins.py:180
|
||||
#: src/baserow/contrib/builder/elements/mixins.py:185
|
||||
#: src/baserow/contrib/builder/elements/mixins.py:190
|
||||
#, python-format
|
||||
msgid "Column %(count)s"
|
||||
msgstr ""
|
||||
|
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-01-18 13:32+0000\n"
|
||||
"POT-Creation-Date: 2024-04-22 17:45+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -49,7 +49,7 @@ msgid ""
|
|||
"\"%(airtable_share_id)s\""
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/application_types.py:231
|
||||
#: src/baserow/contrib/database/application_types.py:236
|
||||
msgid "Table"
|
||||
msgstr ""
|
||||
|
||||
|
@ -62,7 +62,7 @@ msgstr ""
|
|||
msgid "View \"%(view_name)s\" (%(view_id)s) exported to %(export_type)s"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/export/actions.py:82
|
||||
#: src/baserow/contrib/database/export/actions.py:89
|
||||
#, python-format
|
||||
msgid "Table \"%(table_name)s\" (%(table_id)s) exported to %(export_type)s"
|
||||
msgstr ""
|
||||
|
@ -76,43 +76,51 @@ msgstr ""
|
|||
msgid "Field \"%(field_name)s\" (%(field_id)s) updated"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/fields/actions.py:332
|
||||
#: src/baserow/contrib/database/fields/actions.py:339
|
||||
msgid "Create field"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/fields/actions.py:333
|
||||
#: src/baserow/contrib/database/fields/actions.py:340
|
||||
#, python-format
|
||||
msgid "Field \"%(field_name)s\" (%(field_id)s) created"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/fields/actions.py:425
|
||||
#: src/baserow/contrib/database/fields/actions.py:433
|
||||
msgid "Delete field"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/fields/actions.py:426
|
||||
#: src/baserow/contrib/database/fields/actions.py:434
|
||||
#, python-format
|
||||
msgid "Field \"%(field_name)s\" (%(field_id)s) deleted"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/fields/actions.py:493
|
||||
#: src/baserow/contrib/database/fields/actions.py:506
|
||||
msgid "Duplicate field"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/fields/actions.py:495
|
||||
#: src/baserow/contrib/database/fields/actions.py:508
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Field \"%(field_name)s\" (%(field_id)s) duplicated (with_data=%(with_data)s) "
|
||||
"from field \"%(original_field_name)s\" (%(original_field_id)s)"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/fields/models.py:358
|
||||
#: src/baserow/contrib/database/fields/models.py:368
|
||||
#: src/baserow/contrib/database/fields/models.py:507
|
||||
msgid "The format of the duration."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/fields/notification_types.py:45
|
||||
#: src/baserow/contrib/database/fields/notification_types.py:53
|
||||
#, python-format
|
||||
msgid ""
|
||||
"%(sender)s assigned you to %(field_name)s in row %(row_id)s in "
|
||||
"%(sender)s assigned you to %(field_name)s in row %(row_name)s in "
|
||||
"%(table_name)s."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/fields/notification_types.py:181
|
||||
#, python-format
|
||||
msgid ""
|
||||
"%(sender)s mentioned you in %(field_name)s in row %(row_name)s in "
|
||||
"%(table_name)s."
|
||||
msgstr ""
|
||||
|
||||
|
@ -127,8 +135,8 @@ msgstr ""
|
|||
|
||||
#: src/baserow/contrib/database/plugins.py:72
|
||||
#: src/baserow/contrib/database/plugins.py:94
|
||||
#: src/baserow/contrib/database/table/handler.py:376
|
||||
#: src/baserow/contrib/database/table/handler.py:389
|
||||
#: src/baserow/contrib/database/table/handler.py:623
|
||||
#: src/baserow/contrib/database/table/handler.py:636
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
|
@ -137,13 +145,13 @@ msgid "Last name"
|
|||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/plugins.py:74
|
||||
#: src/baserow/contrib/database/table/handler.py:377
|
||||
#: src/baserow/contrib/database/table/handler.py:624
|
||||
msgid "Notes"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/plugins.py:75
|
||||
#: src/baserow/contrib/database/plugins.py:96
|
||||
#: src/baserow/contrib/database/table/handler.py:378
|
||||
#: src/baserow/contrib/database/table/handler.py:625
|
||||
msgid "Active"
|
||||
msgstr ""
|
||||
|
||||
|
@ -180,65 +188,65 @@ msgstr ""
|
|||
msgid "Row (%(row_id)s) created"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/rows/actions.py:112
|
||||
#: src/baserow/contrib/database/rows/actions.py:117
|
||||
msgid "Create rows"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/rows/actions.py:112
|
||||
#: src/baserow/contrib/database/rows/actions.py:117
|
||||
#, python-format
|
||||
msgid "Rows (%(row_ids)s) created"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/rows/actions.py:193
|
||||
#: src/baserow/contrib/database/rows/actions.py:203
|
||||
msgid "Import rows"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/rows/actions.py:193
|
||||
#: src/baserow/contrib/database/rows/actions.py:203
|
||||
#, python-format
|
||||
msgid "Rows (%(row_ids)s) imported"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/rows/actions.py:273
|
||||
#: src/baserow/contrib/database/rows/actions.py:284
|
||||
msgid "Delete row"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/rows/actions.py:273
|
||||
#: src/baserow/contrib/database/rows/actions.py:284
|
||||
#, python-format
|
||||
msgid "Row (%(row_id)s) deleted"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/rows/actions.py:334
|
||||
#: src/baserow/contrib/database/rows/actions.py:350
|
||||
msgid "Delete rows"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/rows/actions.py:334
|
||||
#: src/baserow/contrib/database/rows/actions.py:350
|
||||
#, python-format
|
||||
msgid "Rows (%(row_ids)s) deleted"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/rows/actions.py:476
|
||||
#: src/baserow/contrib/database/rows/actions.py:497
|
||||
msgid "Move row"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/rows/actions.py:476
|
||||
#: src/baserow/contrib/database/rows/actions.py:497
|
||||
#, python-format
|
||||
msgid "Row (%(row_id)s) moved"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/rows/actions.py:585
|
||||
#: src/baserow/contrib/database/rows/actions.py:607
|
||||
msgid "Update row"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/rows/actions.py:585
|
||||
#: src/baserow/contrib/database/rows/actions.py:607
|
||||
#, python-format
|
||||
msgid "Row (%(row_id)s) updated"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/rows/actions.py:685
|
||||
#: src/baserow/contrib/database/rows/actions.py:707
|
||||
msgid "Update rows"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/rows/actions.py:685
|
||||
#: src/baserow/contrib/database/rows/actions.py:707
|
||||
#, python-format
|
||||
msgid "Rows (%(row_ids)s) updated"
|
||||
msgstr ""
|
||||
|
@ -252,50 +260,50 @@ msgstr ""
|
|||
msgid "Table \"%(table_name)s\" (%(table_id)s) created"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/table/actions.py:100
|
||||
#: src/baserow/contrib/database/table/actions.py:104
|
||||
msgid "Delete table"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/table/actions.py:101
|
||||
#: src/baserow/contrib/database/table/actions.py:105
|
||||
#, python-format
|
||||
msgid "Table \"%(table_name)s\" (%(table_id)s) deleted"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/table/actions.py:152
|
||||
#: src/baserow/contrib/database/table/actions.py:160
|
||||
msgid "Order tables"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/table/actions.py:153
|
||||
#: src/baserow/contrib/database/table/actions.py:161
|
||||
msgid "Tables order changed"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/table/actions.py:213
|
||||
#: src/baserow/contrib/database/table/actions.py:224
|
||||
msgid "Update table"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/table/actions.py:215
|
||||
#: src/baserow/contrib/database/table/actions.py:226
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Table (%(table_id)s) name changed from \"%(original_table_name)s\" to "
|
||||
"\"%(table_name)s\""
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/table/actions.py:281
|
||||
#: src/baserow/contrib/database/table/actions.py:296
|
||||
msgid "Duplicate table"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/table/actions.py:283
|
||||
#: src/baserow/contrib/database/table/actions.py:298
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Table \"%(table_name)s\" (%(table_id)s) duplicated from "
|
||||
"\"%(original_table_name)s\" (%(original_table_id)s) "
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/table/handler.py:280
|
||||
#: src/baserow/contrib/database/table/handler.py:527
|
||||
msgid "Grid"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/table/handler.py:338
|
||||
#: src/baserow/contrib/database/table/handler.py:585
|
||||
#, python-format
|
||||
msgid "Field %d"
|
||||
msgstr ""
|
||||
|
@ -310,42 +318,42 @@ msgid ""
|
|||
"A Database Token with name \"%(token_name)s\" (%(token_id)s) has been created"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/tokens/actions.py:54
|
||||
#: src/baserow/contrib/database/tokens/actions.py:58
|
||||
msgid "Update DB token name"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/tokens/actions.py:56
|
||||
#: src/baserow/contrib/database/tokens/actions.py:60
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The Database Token (%(token_name)s) name changed from "
|
||||
"\"%(original_token_name)s\" to \"%(token_name)s\""
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/tokens/actions.py:94
|
||||
#: src/baserow/contrib/database/tokens/actions.py:102
|
||||
msgid "Update DB token permissions"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/tokens/actions.py:96
|
||||
#: src/baserow/contrib/database/tokens/actions.py:104
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The Database Token \"%(token_name)s\" (%(token_id)s) permissions has been "
|
||||
"updated"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/tokens/actions.py:149
|
||||
#: src/baserow/contrib/database/tokens/actions.py:163
|
||||
msgid "Rotate DB token key"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/tokens/actions.py:150
|
||||
#: src/baserow/contrib/database/tokens/actions.py:164
|
||||
#, python-format
|
||||
msgid "The Database Token \"%(token_name)s\" (%(token_id)s) has been rotated"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/tokens/actions.py:182
|
||||
#: src/baserow/contrib/database/tokens/actions.py:200
|
||||
msgid "Delete DB token"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/tokens/actions.py:183
|
||||
#: src/baserow/contrib/database/tokens/actions.py:201
|
||||
#, python-format
|
||||
msgid "The Database Token \"%(token_name)s\" (%(token_id)s) has been deleted"
|
||||
msgstr ""
|
||||
|
@ -359,198 +367,198 @@ msgstr ""
|
|||
msgid "View filter created on field \"%(field_name)s\" (%(field_id)s)"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:143
|
||||
#: src/baserow/contrib/database/views/actions.py:152
|
||||
msgid "Update a view filter"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:144
|
||||
#: src/baserow/contrib/database/views/actions.py:153
|
||||
#, python-format
|
||||
msgid "View filter updated on field \"%(field_name)s\" (%(field_id)s)"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:265
|
||||
#: src/baserow/contrib/database/views/actions.py:284
|
||||
msgid "Delete a view filter"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:266
|
||||
#: src/baserow/contrib/database/views/actions.py:285
|
||||
#, python-format
|
||||
msgid "View filter deleted from field \"%(field_name)s\" (%(field_id)s)"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:366
|
||||
#: src/baserow/contrib/database/views/actions.py:394
|
||||
msgid "Create a view filter group"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:367
|
||||
#: src/baserow/contrib/database/views/actions.py:395
|
||||
msgid "View filter group created"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:443
|
||||
#: src/baserow/contrib/database/views/actions.py:478
|
||||
msgid "Update a view filter group"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:444
|
||||
#: src/baserow/contrib/database/views/actions.py:479
|
||||
#, python-format
|
||||
msgid "View filter group updated to \"%(filter_type)s\""
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:536
|
||||
#: src/baserow/contrib/database/views/actions.py:579
|
||||
msgid "Delete a view filter group"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:537
|
||||
#: src/baserow/contrib/database/views/actions.py:580
|
||||
msgid "View filter group deleted"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:649
|
||||
#: src/baserow/contrib/database/views/actions.py:699
|
||||
msgid "Create a view sort"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:650
|
||||
#: src/baserow/contrib/database/views/actions.py:700
|
||||
#, python-format
|
||||
msgid "View sorted on field \"%(field_name)s\" (%(field_id)s)"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:726
|
||||
#: src/baserow/contrib/database/views/actions.py:784
|
||||
msgid "Update a view sort"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:727
|
||||
#: src/baserow/contrib/database/views/actions.py:785
|
||||
#, python-format
|
||||
msgid "View sort updated on field \"%(field_name)s\" (%(field_id)s)"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:825
|
||||
#: src/baserow/contrib/database/views/actions.py:893
|
||||
msgid "Delete a view sort"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:826
|
||||
#: src/baserow/contrib/database/views/actions.py:894
|
||||
#, python-format
|
||||
msgid "View sort deleted from field \"%(field_name)s\" (%(field_id)s)"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:903
|
||||
#: src/baserow/contrib/database/views/actions.py:979
|
||||
msgid "Order views"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:903
|
||||
#: src/baserow/contrib/database/views/actions.py:979
|
||||
msgid "Views order changed"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:970
|
||||
#: src/baserow/contrib/database/views/actions.py:1050
|
||||
msgid "Update view field options"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:971
|
||||
#: src/baserow/contrib/database/views/actions.py:1051
|
||||
msgid "ViewFieldOptions updated"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:1066
|
||||
#: src/baserow/contrib/database/views/actions.py:1152
|
||||
msgid "View slug URL updated"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:1067
|
||||
#: src/baserow/contrib/database/views/actions.py:1153
|
||||
msgid "View changed public slug URL"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:1136
|
||||
#: src/baserow/contrib/database/views/actions.py:1226
|
||||
msgid "Update view"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:1137
|
||||
#: src/baserow/contrib/database/views/actions.py:1227
|
||||
#, python-format
|
||||
msgid "View \"%(view_name)s\" (%(view_id)s) updated"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:1213
|
||||
#: src/baserow/contrib/database/views/actions.py:1308
|
||||
msgid "Create view"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:1214
|
||||
#: src/baserow/contrib/database/views/actions.py:1309
|
||||
#, python-format
|
||||
msgid "View \"%(view_name)s\" (%(view_id)s) created"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:1282
|
||||
#: src/baserow/contrib/database/views/actions.py:1383
|
||||
msgid "Duplicate view"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:1284
|
||||
#: src/baserow/contrib/database/views/actions.py:1385
|
||||
#, python-format
|
||||
msgid ""
|
||||
"View \"%(view_name)s\" (%(view_id)s) duplicated from view "
|
||||
"\"%(original_view_name)s\" (%(original_view_id)s)"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:1352
|
||||
#: src/baserow/contrib/database/views/actions.py:1459
|
||||
msgid "Delete view"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:1353
|
||||
#: src/baserow/contrib/database/views/actions.py:1460
|
||||
#, python-format
|
||||
msgid "View \"%(view_name)s\" (%(view_id)s) deleted"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:1410
|
||||
#: src/baserow/contrib/database/views/actions.py:1522
|
||||
msgid "Create decoration"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:1411
|
||||
#: src/baserow/contrib/database/views/actions.py:1523
|
||||
#, python-format
|
||||
msgid "View decoration %(decorator_id)s created"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:1506
|
||||
#: src/baserow/contrib/database/views/actions.py:1627
|
||||
msgid "Update decoration"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:1507
|
||||
#: src/baserow/contrib/database/views/actions.py:1628
|
||||
#, python-format
|
||||
msgid "View decoration %(decorator_id)s updated"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:1631
|
||||
#: src/baserow/contrib/database/views/actions.py:1765
|
||||
msgid "Delete decoration"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:1632
|
||||
#: src/baserow/contrib/database/views/actions.py:1766
|
||||
#, python-format
|
||||
msgid "View decoration %(decorator_id)s deleted"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:1717
|
||||
#: src/baserow/contrib/database/views/actions.py:1862
|
||||
msgid "Create a view group"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:1718
|
||||
#: src/baserow/contrib/database/views/actions.py:1863
|
||||
#, python-format
|
||||
msgid "View grouped on field \"%(field_name)s\" (%(field_id)s)"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:1809
|
||||
#: src/baserow/contrib/database/views/actions.py:1963
|
||||
msgid "Update a view group"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:1810
|
||||
#: src/baserow/contrib/database/views/actions.py:1964
|
||||
#, python-format
|
||||
msgid "View group by updated on field \"%(field_name)s\" (%(field_id)s)"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:1925
|
||||
#: src/baserow/contrib/database/views/actions.py:2092
|
||||
msgid "Delete a view group"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/actions.py:1926
|
||||
#: src/baserow/contrib/database/views/actions.py:2093
|
||||
#, python-format
|
||||
msgid "View group by deleted from field \"%(field_name)s\" (%(field_id)s)"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/notification_types.py:83
|
||||
#: src/baserow/contrib/database/views/notification_types.py:84
|
||||
#, python-format
|
||||
msgid "%(form_name)s has been submitted in %(table_name)s"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/views/notification_types.py:100
|
||||
#: src/baserow/contrib/database/views/notification_types.py:101
|
||||
#, python-format
|
||||
msgid "and 1 more field"
|
||||
msgid_plural "and %(count)s more fields"
|
||||
|
@ -568,22 +576,22 @@ msgid ""
|
|||
"to %(webhook_url)s\" created"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/webhooks/actions.py:82
|
||||
#: src/baserow/contrib/database/webhooks/actions.py:88
|
||||
msgid "Delete Webhook"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/webhooks/actions.py:84
|
||||
#: src/baserow/contrib/database/webhooks/actions.py:90
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Webhook \"%(webhook_name)s\" (%(webhook_id)s) as %(webhook_request_method)s "
|
||||
"to %(webhook_url)s\" deleted"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/webhooks/actions.py:140
|
||||
#: src/baserow/contrib/database/webhooks/actions.py:152
|
||||
msgid "Update Webhook"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/contrib/database/webhooks/actions.py:142
|
||||
#: src/baserow/contrib/database/webhooks/actions.py:154
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Webhook \"%(webhook_name)s\" (%(webhook_id)s) as %(webhook_request_method)s "
|
||||
|
|
|
@ -231,8 +231,10 @@ class CoreConfig(AppConfig):
|
|||
ResetUserPasswordActionType,
|
||||
ScheduleUserDeletionActionType,
|
||||
SendResetUserPasswordActionType,
|
||||
SendVerifyEmailAddressActionType,
|
||||
SignInUserActionType,
|
||||
UpdateUserActionType,
|
||||
VerifyEmailAddressActionType,
|
||||
)
|
||||
|
||||
action_type_registry.register(CreateUserActionType())
|
||||
|
@ -243,6 +245,8 @@ class CoreConfig(AppConfig):
|
|||
action_type_registry.register(ChangeUserPasswordActionType())
|
||||
action_type_registry.register(SendResetUserPasswordActionType())
|
||||
action_type_registry.register(ResetUserPasswordActionType())
|
||||
action_type_registry.register(SendVerifyEmailAddressActionType())
|
||||
action_type_registry.register(VerifyEmailAddressActionType())
|
||||
|
||||
from baserow.core.action.scopes import (
|
||||
ApplicationActionScopeType,
|
||||
|
|
|
@ -6,6 +6,10 @@ class AuthProviderDisabled(Exception):
|
|||
"""Raised when it is not possible to use a particular auth provider."""
|
||||
|
||||
|
||||
class EmailVerificationRequired(Exception):
|
||||
"""Raised when the user's email has not been verified yet."""
|
||||
|
||||
|
||||
class DifferentAuthProvider(Exception):
|
||||
"""
|
||||
Raised when logging in an existing user that should not
|
||||
|
|
|
@ -86,6 +86,22 @@ class BaseEmailMessage(EmailMultiAlternatives):
|
|||
transaction.on_commit(lambda: s.send(fail_silently))
|
||||
|
||||
|
||||
class EmailPendingVerificationEmail(BaseEmailMessage):
|
||||
template_name = "baserow/core/user/email_pending_verification.html"
|
||||
|
||||
def __init__(self, confirm_url, *args, **kwargs):
|
||||
self.confirm_url = confirm_url
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_subject(self):
|
||||
return _("Please confirm email")
|
||||
|
||||
def get_context(self):
|
||||
context = super().get_context()
|
||||
context.update(confirm_url=self.confirm_url)
|
||||
return context
|
||||
|
||||
|
||||
class WorkspaceInvitationEmail(BaseEmailMessage):
|
||||
template_name = "baserow/core/workspace_invitation.html"
|
||||
|
||||
|
|
|
@ -172,6 +172,7 @@ class CoreHandler(metaclass=baserow_trace_methods(tracer)):
|
|||
"track_workspace_usage",
|
||||
"show_baserow_help_request",
|
||||
"co_branding_logo",
|
||||
"email_verification",
|
||||
],
|
||||
settings_instance,
|
||||
)
|
||||
|
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-01-18 13:32+0000\n"
|
||||
"POT-Creation-Date: 2024-04-22 17:45+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -23,88 +23,88 @@ msgstr ""
|
|||
msgid "in group \"%(group_name)s\" (%(group_id)s)."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:36
|
||||
#: src/baserow/core/actions.py:35
|
||||
msgid "Delete group"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:37
|
||||
#: src/baserow/core/actions.py:36
|
||||
#, python-format
|
||||
msgid "Group \"%(group_name)s\" (%(group_id)s) deleted."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:97
|
||||
#: src/baserow/core/actions.py:99
|
||||
msgid "Create group"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:98
|
||||
#: src/baserow/core/actions.py:100
|
||||
#, python-format
|
||||
msgid "Group \"%(group_name)s\" (%(group_id)s) created."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:156
|
||||
#: src/baserow/core/actions.py:161
|
||||
msgid "Update group"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:158
|
||||
#: src/baserow/core/actions.py:163
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Group (%(group_id)s) name changed from \"%(original_group_name)s\" to "
|
||||
"\"%(group_name)s.\""
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:237
|
||||
#: src/baserow/core/actions.py:245
|
||||
msgid "Order groups"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:238
|
||||
#: src/baserow/core/actions.py:246
|
||||
msgid "Groups order changed."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:297
|
||||
#: src/baserow/core/actions.py:305
|
||||
msgid "Order applications"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:297
|
||||
#: src/baserow/core/actions.py:305
|
||||
msgid "Applications reordered"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:360
|
||||
#: src/baserow/core/actions.py:371
|
||||
msgid "Create application"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:361
|
||||
#: src/baserow/core/actions.py:372
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\"%(application_name)s\" (%(application_id)s) %(application_type)s created"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:442
|
||||
#: src/baserow/core/actions.py:453
|
||||
msgid "Delete application"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:444
|
||||
#: src/baserow/core/actions.py:455
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Application \"%(application_name)s\" (%(application_id)s) of type "
|
||||
"%(application_type)s deleted"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:506
|
||||
#: src/baserow/core/actions.py:522
|
||||
msgid "Update application"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:508
|
||||
#: src/baserow/core/actions.py:524
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Application (%(application_id)s) of type %(application_type)s renamed from "
|
||||
"\"%(original_application_name)s\" to \"%(application_name)s\""
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:579
|
||||
#: src/baserow/core/actions.py:606
|
||||
msgid "Duplicate application"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:581
|
||||
#: src/baserow/core/actions.py:608
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Application \"%(application_name)s\" (%(application_id)s) of type "
|
||||
|
@ -112,66 +112,66 @@ msgid ""
|
|||
"\" (%(original_application_id)s)"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:658
|
||||
#: src/baserow/core/actions.py:691
|
||||
msgid "Install template"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:660
|
||||
#: src/baserow/core/actions.py:693
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Template \"%(template_name)s\" (%(template_id)s) installed into application "
|
||||
"IDs %(installed_application_ids)s"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:738
|
||||
#: src/baserow/core/actions.py:776
|
||||
msgid "Create group invitation"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:740
|
||||
#: src/baserow/core/actions.py:778
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Group invitation created for \"%(email)s\" to join \"%(group_name)s"
|
||||
"\" (%(group_id)s) as %(permissions)s."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:791
|
||||
#: src/baserow/core/actions.py:833
|
||||
msgid "Delete group invitation"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:793
|
||||
#: src/baserow/core/actions.py:835
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Group invitation (%(invitation_id)s) deleted for \"%(email)s\" to join "
|
||||
"\"%(group_name)s\" (%(group_id)s) as %(permissions)s."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:846
|
||||
#: src/baserow/core/actions.py:893
|
||||
msgid "Accept group invitation"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:848
|
||||
#: src/baserow/core/actions.py:895
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Invitation (%(invitation_id)s) sent by \"%(sender)s\" to join "
|
||||
"\"%(group_name)s\" (%(group_id)s) as %(permissions)s was accepted."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:901
|
||||
#: src/baserow/core/actions.py:953
|
||||
msgid "Reject group invitation"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:903
|
||||
#: src/baserow/core/actions.py:955
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Invitation (%(invitation_id)s) sent by \"%(sender)s\" to join "
|
||||
"\"%(group_name)s\" (%(group_id)s) as %(permissions)s was rejected."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:956
|
||||
#: src/baserow/core/actions.py:1013
|
||||
msgid "Update group invitation permissions"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:958
|
||||
#: src/baserow/core/actions.py:1015
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Invitation (%(invitation_id)s) permissions changed from "
|
||||
|
@ -179,35 +179,40 @@ msgid ""
|
|||
"\"%(group_name)s\" (%(group_id)s)."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:1015
|
||||
#: src/baserow/core/actions.py:1078
|
||||
msgid "Leave group"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/actions.py:1016
|
||||
#: src/baserow/core/actions.py:1079
|
||||
#, python-format
|
||||
msgid "Group \"%(group_name)s\" (%(group_id)s) left."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/emails.py:103
|
||||
#: src/baserow/core/emails.py:97
|
||||
#: src/baserow/core/templates/baserow/core/user/email_pending_verification.html:176
|
||||
msgid "Please confirm email"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/emails.py:115
|
||||
#, python-format
|
||||
msgid "%(by)s invited you to %(workspace_name)s - Baserow"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/emails.py:136
|
||||
#: src/baserow/core/emails.py:148
|
||||
msgid "You have 1 new notification - Baserow"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/emails.py:138
|
||||
#: src/baserow/core/emails.py:150
|
||||
#, python-format
|
||||
msgid "You have %(count)d new notifications - Baserow"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/notification_types.py:94
|
||||
#: src/baserow/core/notification_types.py:92
|
||||
#, python-format
|
||||
msgid "%(user)s accepted your invitation to collaborate to %(workspace_name)s."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/notification_types.py:135
|
||||
#: src/baserow/core/notification_types.py:133
|
||||
#, python-format
|
||||
msgid "%(user)s rejected your invitation to collaborate to %(workspace_name)s."
|
||||
msgstr ""
|
||||
|
@ -223,11 +228,11 @@ msgid ""
|
|||
"\"%(application_name)s\" (%(application_id)s)."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/snapshots/actions.py:68
|
||||
#: src/baserow/core/snapshots/actions.py:72
|
||||
msgid "Restore Snapshot"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/snapshots/actions.py:70
|
||||
#: src/baserow/core/snapshots/actions.py:74
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Snapshot \"%(snapshot_name)s\" (%(snapshot_id)s) restored from application "
|
||||
|
@ -235,119 +240,116 @@ msgid ""
|
|||
"application \"%(application_name)s\" (%(application_id)s)."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/snapshots/actions.py:124
|
||||
#: src/baserow/core/snapshots/actions.py:133
|
||||
msgid "Delete Snapshot"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/snapshots/actions.py:126
|
||||
#: src/baserow/core/snapshots/actions.py:135
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Snapshot \"%(snapshot_name)s\" (%(snapshot_id)s) deleted for application "
|
||||
"\"%(application_name)s\" (%(application_id)s)."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/templates/baserow/core/group_invitation.html:144
|
||||
#: src/baserow/core/templates/baserow/core/workspace_invitation.html:144
|
||||
msgid "Invitation"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/templates/baserow/core/group_invitation.html:149
|
||||
#, python-format
|
||||
msgid ""
|
||||
"<strong>%(first_name)s</strong> has invited you to collaborate on <strong>"
|
||||
"%(group_name)s</strong>."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/templates/baserow/core/group_invitation.html:163
|
||||
#: src/baserow/core/templates/baserow/core/workspace_invitation.html:167
|
||||
msgid "Accept invitation"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/templates/baserow/core/group_invitation.html:177
|
||||
#: src/baserow/core/templates/baserow/core/notifications_summary.html:212
|
||||
#: src/baserow/core/templates/baserow/core/user/account_deleted.html:154
|
||||
#: src/baserow/core/templates/baserow/core/user/account_deletion_cancelled.html:154
|
||||
#: src/baserow/core/templates/baserow/core/user/account_deletion_scheduled.html:159
|
||||
#: src/baserow/core/templates/baserow/core/user/reset_password.html:177
|
||||
#: src/baserow/core/templates/baserow/core/workspace_invitation.html:181
|
||||
msgid ""
|
||||
"Baserow is an open source no-code database tool which allows you to "
|
||||
"collaborate on projects, customers and more. It gives you the powers of a "
|
||||
"developer without leaving your browser."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/templates/baserow/core/notifications_summary.html:146
|
||||
#: src/baserow/core/templates/baserow/core/notifications_summary.html:176
|
||||
#, python-format
|
||||
msgid "You have %(counter)s new notification"
|
||||
msgid_plural "You have %(counter)s new notifications"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: src/baserow/core/templates/baserow/core/notifications_summary.html:187
|
||||
#: src/baserow/core/templates/baserow/core/notifications_summary.html:228
|
||||
#, python-format
|
||||
msgid "Plus %(counter)s more notification."
|
||||
msgid_plural "Plus %(counter)s more notifications."
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: src/baserow/core/templates/baserow/core/notifications_summary.html:198
|
||||
#: src/baserow/core/templates/baserow/core/notifications_summary.html:239
|
||||
msgid "View in Baserow"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/templates/baserow/core/user/account_deleted.html:144
|
||||
#: src/baserow/core/templates/baserow/core/notifications_summary.html:253
|
||||
#: src/baserow/core/templates/baserow/core/user/account_deleted.html:186
|
||||
#: src/baserow/core/templates/baserow/core/user/account_deletion_cancelled.html:186
|
||||
#: src/baserow/core/templates/baserow/core/user/account_deletion_scheduled.html:191
|
||||
#: src/baserow/core/templates/baserow/core/user/reset_password.html:209
|
||||
#: src/baserow/core/templates/baserow/core/workspace_invitation.html:213
|
||||
msgid ""
|
||||
"Baserow is an open source no-code database tool which allows you to "
|
||||
"collaborate on projects, customers and more. It gives you the powers of a "
|
||||
"developer without leaving your browser."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/templates/baserow/core/user/account_deleted.html:176
|
||||
msgid "Account permanently deleted"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/templates/baserow/core/user/account_deleted.html:149
|
||||
#: src/baserow/core/templates/baserow/core/user/account_deleted.html:181
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Your account (%(username)s) on Baserow (%(public_web_frontend_hostname)s) "
|
||||
"Your account (%(username)s) on Baserow (%(baserow_embedded_share_hostname)s) "
|
||||
"has been permanently deleted."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/templates/baserow/core/user/account_deletion_cancelled.html:144
|
||||
#: src/baserow/core/templates/baserow/core/user/account_deletion_cancelled.html:176
|
||||
msgid "Account deletion cancelled"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/templates/baserow/core/user/account_deletion_cancelled.html:149
|
||||
#: src/baserow/core/templates/baserow/core/user/account_deletion_cancelled.html:181
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Your account (%(username)s) on Baserow (%(public_web_frontend_hostname)s) "
|
||||
"Your account (%(username)s) on Baserow (%(baserow_embedded_share_hostname)s) "
|
||||
"was pending deletion, but you've logged in so this operation has been "
|
||||
"cancelled."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/templates/baserow/core/user/account_deletion_scheduled.html:144
|
||||
#: src/baserow/core/templates/baserow/core/user/account_deletion_scheduled.html:176
|
||||
msgid "Account pending deletion"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/templates/baserow/core/user/account_deletion_scheduled.html:149
|
||||
#: src/baserow/core/templates/baserow/core/user/account_deletion_scheduled.html:181
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Your account (%(username)s) on Baserow (%(public_web_frontend_hostname)s) "
|
||||
"Your account (%(username)s) on Baserow (%(baserow_embedded_share_hostname)s) "
|
||||
"will be permanently deleted in %(days_left)s days."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/templates/baserow/core/user/account_deletion_scheduled.html:154
|
||||
#: src/baserow/core/templates/baserow/core/user/account_deletion_scheduled.html:186
|
||||
msgid ""
|
||||
"If you've changed your mind and want to cancel your account deletion, you "
|
||||
"just have to login again."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/templates/baserow/core/user/reset_password.html:144
|
||||
#: src/baserow/core/templates/baserow/core/user/reset_password.html:163
|
||||
#: src/baserow/core/templates/baserow/core/user/email_pending_verification.html:181
|
||||
msgid "Thank you for using Baserow"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/templates/baserow/core/user/email_pending_verification.html:186
|
||||
msgid ""
|
||||
"To keep your account secure, please take a moment to verify your email by "
|
||||
"clicking the button below. Your email address will be used to assist you in "
|
||||
"changing your Baserow password should you ever need to in the future."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/templates/baserow/core/user/email_pending_verification.html:195
|
||||
msgid "Confirm"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/templates/baserow/core/user/reset_password.html:176
|
||||
#: src/baserow/core/templates/baserow/core/user/reset_password.html:195
|
||||
msgid "Reset password"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/templates/baserow/core/user/reset_password.html:149
|
||||
#: src/baserow/core/templates/baserow/core/user/reset_password.html:181
|
||||
#, python-format
|
||||
msgid ""
|
||||
"A password reset was requested for your account (%(username)s) on Baserow "
|
||||
"(%(public_web_frontend_hostname)s). If you did not authorize this, you may "
|
||||
"simply ignore this email."
|
||||
"(%(baserow_embedded_share_hostname)s). If you did not authorize this, you "
|
||||
"may simply ignore this email."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/templates/baserow/core/user/reset_password.html:154
|
||||
#: src/baserow/core/templates/baserow/core/user/reset_password.html:186
|
||||
#, python-format
|
||||
msgid ""
|
||||
"To continue with your password reset, simply click the button below, and you "
|
||||
|
@ -355,13 +357,21 @@ msgid ""
|
|||
"hours."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/templates/baserow/core/workspace_invitation.html:149
|
||||
#: src/baserow/core/templates/baserow/core/workspace_invitation.html:176
|
||||
msgid "Invitation"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/templates/baserow/core/workspace_invitation.html:181
|
||||
#, python-format
|
||||
msgid ""
|
||||
"<strong>%(first_name)s</strong> has invited you to collaborate on <strong>"
|
||||
"%(workspace_name)s</strong>."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/templates/baserow/core/workspace_invitation.html:199
|
||||
msgid "Accept invitation"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/trash/actions.py:20
|
||||
msgid "Empty trash"
|
||||
msgstr ""
|
||||
|
@ -373,18 +383,18 @@ msgid ""
|
|||
"emptied"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/trash/actions.py:79
|
||||
#: src/baserow/core/trash/actions.py:83
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Trash for workspace \"%(workspace_name)s\" (%(workspace_id)s) has been "
|
||||
"emptied."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/trash/actions.py:90
|
||||
#: src/baserow/core/trash/actions.py:94
|
||||
msgid "Restore from trash"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/trash/actions.py:91
|
||||
#: src/baserow/core/trash/actions.py:95
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Item of type \"%(item_type)s\" (%(item_id)s) has been restored from trash"
|
||||
|
@ -402,75 +412,93 @@ msgid ""
|
|||
"%(with_invitation_token)s)"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/user/actions.py:110
|
||||
#: src/baserow/core/user/actions.py:117
|
||||
msgid "Update User"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/user/actions.py:111
|
||||
#: src/baserow/core/user/actions.py:118
|
||||
#, python-format
|
||||
msgid "User \"%(user_email)s\" (%(user_id)s) updated"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/user/actions.py:163
|
||||
#: src/baserow/core/user/actions.py:174
|
||||
msgid "Schedule user deletion"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/user/actions.py:165
|
||||
#: src/baserow/core/user/actions.py:176
|
||||
#, python-format
|
||||
msgid ""
|
||||
"User \"%(user_email)s\" (%(user_id)s) scheduled to be deleted after grace "
|
||||
"time"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/user/actions.py:196
|
||||
#: src/baserow/core/user/actions.py:210
|
||||
msgid "Cancel user deletion"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/user/actions.py:198
|
||||
#: src/baserow/core/user/actions.py:212
|
||||
#, python-format
|
||||
msgid ""
|
||||
"User \"%(user_email)s\" (%(user_id)s) logged in cancelling the deletion "
|
||||
"process"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/user/actions.py:229
|
||||
#: src/baserow/core/user/actions.py:246
|
||||
msgid "Sign In User"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/user/actions.py:231
|
||||
#: src/baserow/core/user/actions.py:248
|
||||
#, python-format
|
||||
msgid ""
|
||||
"User \"%(user_email)s\" (%(user_id)s) signed in via \"%(auth_provider_type)s"
|
||||
"\" (%(auth_provider_id)s) auth provider"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/user/actions.py:283
|
||||
#: src/baserow/core/user/actions.py:305
|
||||
msgid "Send reset user password"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/user/actions.py:284
|
||||
#: src/baserow/core/user/actions.py:306
|
||||
#, python-format
|
||||
msgid "User \"%(user_email)s\" (%(user_id)s) requested to reset password"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/user/actions.py:314
|
||||
#: src/baserow/core/user/actions.py:339
|
||||
msgid "Change user password"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/user/actions.py:315
|
||||
#: src/baserow/core/user/actions.py:340
|
||||
#, python-format
|
||||
msgid "User \"%(user_email)s\" (%(user_id)s) changed password"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/user/actions.py:351
|
||||
#: src/baserow/core/user/actions.py:379
|
||||
msgid "Reset user password"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/user/actions.py:352
|
||||
#: src/baserow/core/user/actions.py:380
|
||||
#, python-format
|
||||
msgid "User \"%(user_email)s\" (%(user_id)s) reset password"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/user/actions.py:416
|
||||
msgid "Send verify email"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/user/actions.py:417
|
||||
#, python-format
|
||||
msgid "User \"%(user_email)s\" (%(user_id)s) requested to verify email"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/user/actions.py:450
|
||||
msgid "Verify email"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/user/actions.py:451
|
||||
#, python-format
|
||||
msgid "User \"%(user_email)s\" (%(user_id)s) verify email"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/user/emails.py:16
|
||||
msgid "Reset password - Baserow"
|
||||
msgstr ""
|
||||
|
@ -487,7 +515,7 @@ msgstr ""
|
|||
msgid "Account deletion cancelled - Baserow"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow/core/user/handler.py:248
|
||||
#: src/baserow/core/user/handler.py:252
|
||||
#, python-format
|
||||
msgid "%(name)s's workspace"
|
||||
msgstr ""
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
# Generated by Django 4.1.13 on 2024-04-03 09:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("core", "0085_workspace_generative_ai_models_settings"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="settings",
|
||||
name="email_verification",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("no_verification", "no_verification"),
|
||||
("recommended", "recommended"),
|
||||
("enforced", "enforced"),
|
||||
],
|
||||
default="no_verification",
|
||||
help_text="Controls whether user email addresses have to be verified.",
|
||||
max_length=16,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="userprofile",
|
||||
name="email_verified",
|
||||
field=models.BooleanField(default=False, null=True),
|
||||
),
|
||||
]
|
|
@ -80,6 +80,13 @@ class Settings(models.Model):
|
|||
change. This table can only contain a single row.
|
||||
"""
|
||||
|
||||
# Keep these in sync with the web-frontend options in
|
||||
# web-frontend/modules/core/enums.js
|
||||
class EmailVerificationOptions(models.TextChoices):
|
||||
NO_VERIFICATION = "no_verification", "no_verification"
|
||||
RECOMMENDED = "recommended", "recommended"
|
||||
ENFORCED = "enforced", "enforced"
|
||||
|
||||
instance_id = models.SlugField(default=secrets.token_urlsafe)
|
||||
allow_new_signups = models.BooleanField(
|
||||
default=True,
|
||||
|
@ -131,6 +138,14 @@ class Settings(models.Model):
|
|||
related_name="+",
|
||||
help_text="Co-branding logo that's placed next to the Baserow logo (176x29).",
|
||||
)
|
||||
# TODO Remove null=True in a future release.
|
||||
email_verification = models.TextField(
|
||||
max_length=16,
|
||||
null=True,
|
||||
choices=EmailVerificationOptions.choices,
|
||||
default=EmailVerificationOptions.NO_VERIFICATION,
|
||||
help_text="Controls whether user email addresses have to be verified.",
|
||||
)
|
||||
|
||||
|
||||
class UserProfile(models.Model):
|
||||
|
@ -188,6 +203,8 @@ class UserProfile(models.Model):
|
|||
default=None,
|
||||
help_text="Timestamp when the user changed their password.",
|
||||
)
|
||||
# TODO Remove null=True in a future release.
|
||||
email_verified = models.BooleanField(null=True, default=False)
|
||||
|
||||
def iat_before_last_password_change(self, iat: int) -> bool:
|
||||
"""
|
||||
|
|
|
@ -0,0 +1,220 @@
|
|||
{% load i18n %}
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
|
||||
<head>
|
||||
<title>
|
||||
</title>
|
||||
<!--[if !mso]><!-->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
#outlook a {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix { width:100% !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Inter:400,600" rel="stylesheet" type="text/css">
|
||||
<style type="text/css">
|
||||
@import url(https://fonts.googleapis.com/css?family=Inter:400,600);
|
||||
</style>
|
||||
<!--<![endif]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width:480px) {
|
||||
.mj-column-px-190 {
|
||||
width: 190px !important;
|
||||
max-width: 190px;
|
||||
}
|
||||
|
||||
.mj-column-per-50 {
|
||||
width: 50% !important;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style media="screen and (min-width:480px)">
|
||||
.moz-text-html .mj-column-px-190 {
|
||||
width: 190px !important;
|
||||
max-width: 190px;
|
||||
}
|
||||
|
||||
.moz-text-html .mj-column-per-50 {
|
||||
width: 50% !important;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.moz-text-html .mj-column-per-100 {
|
||||
width: 100% !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
@media only screen and (max-width:480px) {
|
||||
table.mj-full-width-mobile {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
td.mj-full-width-mobile {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="word-spacing:normal;">
|
||||
<div style="">
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:left;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:190px;" ><![endif]-->
|
||||
<div class="mj-column-px-190 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;padding-bottom:0;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:140px;">
|
||||
<a href="{{ baserow_embedded_share_url }}" target="_blank">
|
||||
<img height="auto" src="{{ logo_url }}" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;" width="140" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td><td class="" style="vertical-align:top;width:300px;" ><![endif]-->
|
||||
<div class="mj-column-per-50 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;padding-left:0;word-break:break-word;">
|
||||
<div style="font-family:Inter,sans-serif;font-size:13px;line-height:170%;text-align:left;color:#070810;">{{ logo_additional_text }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Inter,sans-serif;font-size:22px;font-weight:600;line-height:1;text-align:left;color:#070810;">{% trans "Please confirm email" %}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Inter,sans-serif;font-size:13px;line-height:170%;text-align:left;color:#070810;">{% blocktrans trimmed %} Thank you for using Baserow {% endblocktrans %}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Inter,sans-serif;font-size:13px;line-height:170%;text-align:left;color:#070810;">{% blocktrans trimmed %} To keep your account secure, please take a moment to verify your email by clicking the button below. Your email address will be used to assist you in changing your Baserow password should you ever need to in the future. {% endblocktrans %}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" bgcolor="#5190ef" role="presentation" style="border:none;border-radius:4px;cursor:auto;mso-padding-alt:12px 30px;background:#5190ef;" valign="middle">
|
||||
<a href="{{ confirm_url }}" style="display:inline-block;background:#5190ef;color:#ffffff;font-family:Inter,sans-serif;font-size:15px;font-weight:600;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:12px 30px;mso-padding-alt:0px;border-radius:4px;" target="_blank"> {% trans "Confirm" %} </a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Inter,sans-serif;font-size:12px;line-height:1;text-align:left;color:#9c9c9f;">{{ confirm_url }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,23 @@
|
|||
<% layout("../../base.layout.eta") %>
|
||||
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text mj-class="title">{% trans "Please confirm email" %}</mj-text>
|
||||
<mj-text mj-class="text">
|
||||
{% blocktrans trimmed %}
|
||||
Thank you for using Baserow
|
||||
{% endblocktrans %}
|
||||
</mj-text>
|
||||
<mj-text mj-class="text">
|
||||
{% blocktrans trimmed %}
|
||||
To keep your account secure, please take a moment to verify your email by clicking the button below. Your email address will be used to assist you in changing your Baserow password should you ever need to in the future.
|
||||
{% endblocktrans %}
|
||||
</mj-text>
|
||||
<mj-button mj-class="button mt-20" href="{{ confirm_url }}">
|
||||
{% trans "Confirm" %}
|
||||
</mj-button>
|
||||
<mj-text mj-class="button-url">
|
||||
{{ confirm_url }}
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
|
@ -12,7 +12,7 @@ from baserow.core.action.registries import (
|
|||
from baserow.core.action.scopes import RootActionScopeType
|
||||
from baserow.core.auth_provider.handler import PasswordProviderHandler
|
||||
from baserow.core.auth_provider.models import AuthProviderModel
|
||||
from baserow.core.models import Template
|
||||
from baserow.core.models import Template, User
|
||||
from baserow.core.registries import auth_provider_type_registry
|
||||
from baserow.core.user.handler import UserHandler
|
||||
|
||||
|
@ -408,3 +408,75 @@ class ResetUserPasswordActionType(ActionType):
|
|||
@classmethod
|
||||
def scope(cls) -> ActionScopeStr:
|
||||
return RootActionScopeType.value()
|
||||
|
||||
|
||||
class SendVerifyEmailAddressActionType(ActionType):
|
||||
type = "send_verify_email"
|
||||
description = ActionTypeDescription(
|
||||
_("Send verify email"),
|
||||
_('User "%(user_email)s" (%(user_id)s) requested to verify email'),
|
||||
)
|
||||
analytics_params = [
|
||||
"user_id",
|
||||
]
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Params:
|
||||
user_id: int
|
||||
user_email: str
|
||||
|
||||
@classmethod
|
||||
def do(cls, user: User):
|
||||
"""
|
||||
Sends an email verification email to the user.
|
||||
"""
|
||||
|
||||
UserHandler().send_email_pending_verification(user)
|
||||
|
||||
cls.register_action(
|
||||
user=user, params=cls.Params(user.id, user.email), scope=cls.scope()
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
@classmethod
|
||||
def scope(cls) -> ActionScopeStr:
|
||||
return RootActionScopeType.value()
|
||||
|
||||
|
||||
class VerifyEmailAddressActionType(ActionType):
|
||||
type = "verify_email"
|
||||
description = ActionTypeDescription(
|
||||
_("Verify email"),
|
||||
_('User "%(user_email)s" (%(user_id)s) verify email'),
|
||||
)
|
||||
analytics_params = [
|
||||
"user_id",
|
||||
]
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Params:
|
||||
user_id: int
|
||||
user_email: str
|
||||
|
||||
@classmethod
|
||||
def do(cls, verification_token: str):
|
||||
"""
|
||||
Confirm that the associated email address
|
||||
belongs to the user.
|
||||
|
||||
:param verification_token: The secret token to
|
||||
verify that the email belongs to the user.
|
||||
"""
|
||||
|
||||
user = UserHandler().verify_email_address(verification_token)
|
||||
|
||||
cls.register_action(
|
||||
user=user, params=cls.Params(user.id, user.email), scope=cls.scope()
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
@classmethod
|
||||
def scope(cls) -> ActionScopeStr:
|
||||
return RootActionScopeType.value()
|
||||
|
|
|
@ -6,6 +6,14 @@ class UserAlreadyExist(Exception):
|
|||
"""Raised when a user could not be created because the email already exists."""
|
||||
|
||||
|
||||
class InvalidVerificationToken(Exception):
|
||||
"""Raised when the provided token is invalid."""
|
||||
|
||||
|
||||
class EmailAlreadyVerified(Exception):
|
||||
"""Raised when the user's email is verified already."""
|
||||
|
||||
|
||||
class PasswordDoesNotMatchValidation(Exception):
|
||||
"""Raised when the provided password does not match validation."""
|
||||
|
||||
|
|
|
@ -14,11 +14,12 @@ from django.db.utils import IntegrityError
|
|||
from django.utils import timezone, translation
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from itsdangerous import URLSafeTimedSerializer
|
||||
from itsdangerous import URLSafeSerializer, URLSafeTimedSerializer
|
||||
from opentelemetry import trace
|
||||
|
||||
from baserow.core.auth_provider.handler import PasswordProviderHandler
|
||||
from baserow.core.auth_provider.models import AuthProviderModel
|
||||
from baserow.core.emails import EmailPendingVerificationEmail
|
||||
from baserow.core.exceptions import (
|
||||
BaseURLHostnameNotAllowed,
|
||||
WorkspaceInvitationEmailMismatch,
|
||||
|
@ -26,6 +27,7 @@ from baserow.core.exceptions import (
|
|||
from baserow.core.handler import CoreHandler
|
||||
from baserow.core.models import (
|
||||
BlacklistedToken,
|
||||
Settings,
|
||||
Template,
|
||||
UserLogEntry,
|
||||
UserProfile,
|
||||
|
@ -53,7 +55,9 @@ from .emails import (
|
|||
from .exceptions import (
|
||||
DeactivatedUserException,
|
||||
DisabledSignupError,
|
||||
EmailAlreadyVerified,
|
||||
InvalidPassword,
|
||||
InvalidVerificationToken,
|
||||
PasswordDoesNotMatchValidation,
|
||||
RefreshTokenAlreadyBlacklisted,
|
||||
ResetPasswordDisabledError,
|
||||
|
@ -238,6 +242,9 @@ class UserHandler(metaclass=baserow_trace_methods(tracer)):
|
|||
workspace_user = core_handler.accept_workspace_invitation(
|
||||
user, workspace_invitation
|
||||
)
|
||||
profile = user.profile
|
||||
profile.email_verified = True
|
||||
profile.save()
|
||||
|
||||
# If we still don't have a `WorkspaceUser`, which will be because we weren't
|
||||
# invited to a workspace, and `allow_global_workspace_creation` is enabled,
|
||||
|
@ -264,6 +271,17 @@ class UserHandler(metaclass=baserow_trace_methods(tracer)):
|
|||
auth_provider = PasswordProviderHandler.get()
|
||||
auth_provider.user_signed_in(user)
|
||||
|
||||
settings = CoreHandler().get_settings()
|
||||
email_verification_send_email_if = [
|
||||
Settings.EmailVerificationOptions.RECOMMENDED,
|
||||
Settings.EmailVerificationOptions.ENFORCED,
|
||||
]
|
||||
if (
|
||||
settings.email_verification in email_verification_send_email_if
|
||||
and user.profile.email_verified is False
|
||||
):
|
||||
UserHandler().send_email_pending_verification(user)
|
||||
|
||||
return user
|
||||
|
||||
def update_user(
|
||||
|
@ -384,6 +402,7 @@ class UserHandler(metaclass=baserow_trace_methods(tracer)):
|
|||
# Update the last password change timestamp to invalidate old authentication
|
||||
# tokens.
|
||||
user.profile.last_password_change = timezone.now()
|
||||
user.profile.email_verified = True
|
||||
user.profile.save()
|
||||
|
||||
user_password_changed.send(
|
||||
|
@ -642,3 +661,91 @@ class UserHandler(metaclass=baserow_trace_methods(tracer)):
|
|||
|
||||
hashed_token = generate_hash(refresh_token)
|
||||
return BlacklistedToken.objects.filter(hashed_token=hashed_token).exists()
|
||||
|
||||
def _get_email_verification_signer(self) -> URLSafeSerializer:
|
||||
return URLSafeSerializer(settings.SECRET_KEY, "verify-email")
|
||||
|
||||
def create_email_verification_token(self, user: User) -> str:
|
||||
"""
|
||||
Creates email verification token for the provided user
|
||||
based on the current user's email address.
|
||||
|
||||
:param user: The user for which the token should be generated.
|
||||
"""
|
||||
|
||||
signer = self._get_email_verification_signer()
|
||||
expires_at = datetime.datetime.now(datetime.timezone.utc) + timedelta(days=1)
|
||||
return signer.dumps(
|
||||
{
|
||||
"user_id": user.id,
|
||||
"email": user.email,
|
||||
"expires_at": expires_at.isoformat(),
|
||||
}
|
||||
)
|
||||
|
||||
def verify_email_address(self, token: str) -> User:
|
||||
"""
|
||||
Marks the user's email address as verified if the
|
||||
provided token is a valid token that confirms the user's
|
||||
email address.
|
||||
|
||||
:param token: The verification token.
|
||||
:raises InvalidVerificationToken: Raised when the token is
|
||||
expired, the user doesn't exist, or the user's email
|
||||
address doesn't match the one the token was issued for.
|
||||
:raises EmailAlreadyVerified: Raised when the user's
|
||||
email is verified already.
|
||||
"""
|
||||
|
||||
signer = self._get_email_verification_signer()
|
||||
token_data = signer.loads(token)
|
||||
|
||||
if datetime.datetime.fromisoformat(
|
||||
token_data["expires_at"]
|
||||
) < datetime.datetime.now(tz=timezone.utc):
|
||||
raise InvalidVerificationToken()
|
||||
|
||||
try:
|
||||
user = self.get_active_user(user_id=token_data["user_id"])
|
||||
except UserNotFound as ex:
|
||||
raise InvalidVerificationToken() from ex
|
||||
|
||||
if user.profile.email_verified:
|
||||
raise EmailAlreadyVerified()
|
||||
|
||||
if user.email != token_data["email"]:
|
||||
raise InvalidVerificationToken()
|
||||
|
||||
user_profile = user.profile
|
||||
user_profile.email_verified = True
|
||||
user_profile.save()
|
||||
return user
|
||||
|
||||
def send_email_pending_verification(self, user: User):
|
||||
"""
|
||||
Sends out a pending verification email to the user.
|
||||
|
||||
:user: User that should receive the verification email.
|
||||
:raises EmailAlreadyVerified: Raised when the user's
|
||||
email is verified already.
|
||||
"""
|
||||
|
||||
if user.profile.email_verified:
|
||||
raise EmailAlreadyVerified()
|
||||
|
||||
token = self.create_email_verification_token(user)
|
||||
|
||||
base_url = (
|
||||
settings.BASEROW_EMBEDDED_SHARE_URL or settings.PUBLIC_WEB_FRONTEND_URL
|
||||
)
|
||||
if not base_url.endswith("/"):
|
||||
base_url += "/"
|
||||
|
||||
confirm_url = urljoin(base_url, f"verify-email-address/{token}")
|
||||
|
||||
with translation.override(user.profile.language):
|
||||
email = EmailPendingVerificationEmail(
|
||||
to=[user.email],
|
||||
confirm_url=confirm_url,
|
||||
)
|
||||
email.send(fail_silently=False)
|
||||
|
|
|
@ -21,18 +21,26 @@ def normalize_email_address(email):
|
|||
|
||||
|
||||
def generate_session_tokens_for_user(
|
||||
user: AbstractUser, include_refresh_token: bool = False
|
||||
user: AbstractUser,
|
||||
include_refresh_token: bool = False,
|
||||
verified_email_claim: Optional[str] = None,
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
Generates a new access and refresh token (if requested) for the given user.
|
||||
|
||||
:param user: The user for which the tokens must be generated.
|
||||
:param include_refresh_token: Whether or not a refresh token must be included.
|
||||
:param verified_email_claim: Optionally stores which authentication
|
||||
method was used.
|
||||
:return: A dictionary with the access and refresh token.
|
||||
"""
|
||||
|
||||
access_token = AccessToken.for_user(user)
|
||||
refresh_token = RefreshToken.for_user(user) if include_refresh_token else None
|
||||
|
||||
if refresh_token and verified_email_claim is not None:
|
||||
refresh_token["verified_email_claim"] = verified_email_claim
|
||||
|
||||
return prepare_user_tokens_payload(access_token, refresh_token)
|
||||
|
||||
|
||||
|
|
|
@ -32,6 +32,15 @@ def test_get_settings(api_client):
|
|||
response_json = response.json()
|
||||
assert "instance_id" not in response_json
|
||||
assert response_json["allow_new_signups"] is False
|
||||
assert response_json["email_verification"] == "no_verification"
|
||||
|
||||
# None is not returned as a value for email_verification
|
||||
settings.email_verification = None
|
||||
settings.save()
|
||||
response = api_client.get(reverse("api:settings:get"))
|
||||
assert response.status_code == HTTP_200_OK
|
||||
response_json = response.json()
|
||||
assert response_json["email_verification"] == "no_verification"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
@ -235,3 +244,34 @@ def test_register_settings_data_type(api_client, data_fixture):
|
|||
response_json = response.json()
|
||||
assert len(response_json.keys()) > 1
|
||||
assert response_json["test_tmp"] == "hello"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [None, "invalid"])
|
||||
@pytest.mark.django_db
|
||||
def test_update_email_verification_settings_invalid(api_client, data_fixture, value):
|
||||
user, token = data_fixture.create_user_and_token(is_staff=True)
|
||||
|
||||
response = api_client.patch(
|
||||
reverse("api:settings:update"),
|
||||
{"email_verification": value},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response_json["error"] == "ERROR_REQUEST_BODY_VALIDATION"
|
||||
assert response_json["detail"]["email_verification"][0]["code"] == "invalid_choice"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_email_verification_settings(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token(is_staff=True)
|
||||
|
||||
response = api_client.patch(
|
||||
reverse("api:settings:update"),
|
||||
{"email_verification": "recommended"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert CoreHandler().get_settings().email_verification == "recommended"
|
||||
|
|
|
@ -14,9 +14,11 @@ from rest_framework.status import (
|
|||
)
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
from baserow.core.models import BlacklistedToken, UserLogEntry
|
||||
from baserow.core.handler import CoreHandler
|
||||
from baserow.core.models import BlacklistedToken, Settings, UserLogEntry
|
||||
from baserow.core.registries import Plugin, plugin_registry
|
||||
from baserow.core.user.handler import UserHandler
|
||||
from baserow.core.user.utils import generate_session_tokens_for_user
|
||||
from baserow.core.utils import generate_hash
|
||||
|
||||
User = get_user_model()
|
||||
|
@ -197,6 +199,44 @@ def test_token_auth(api_client, data_fixture):
|
|||
assert "user" in response_json
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_token_auth_email_verification_required(api_client, data_fixture):
|
||||
data_fixture.create_password_provider()
|
||||
user = data_fixture.create_user(email="test@example.com", password="password")
|
||||
settings = CoreHandler().get_settings()
|
||||
settings.email_verification = Settings.EmailVerificationOptions.ENFORCED
|
||||
settings.save()
|
||||
|
||||
response = api_client.post(
|
||||
reverse("api:user:token_auth"),
|
||||
{"email": "test@example.com", "password": "password"},
|
||||
format="json",
|
||||
)
|
||||
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_401_UNAUTHORIZED
|
||||
assert response_json["error"] == "ERROR_EMAIL_VERIFICATION_REQUIRED"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_token_auth_email_verification_not_required(api_client, data_fixture):
|
||||
data_fixture.create_password_provider()
|
||||
user = data_fixture.create_user(email="test@example.com", password="password")
|
||||
settings = CoreHandler().get_settings()
|
||||
settings.email_verification = Settings.EmailVerificationOptions.RECOMMENDED
|
||||
settings.save()
|
||||
|
||||
response = api_client.post(
|
||||
reverse("api:user:token_auth"),
|
||||
{"email": "test@example.com", "password": "password"},
|
||||
format="json",
|
||||
)
|
||||
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert "access_token" in response_json
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_token_password_auth_disabled(api_client, data_fixture):
|
||||
data_fixture.create_password_provider(enabled=False)
|
||||
|
@ -340,6 +380,108 @@ def test_refresh_token_is_invalidated_after_password_change(api_client, data_fix
|
|||
assert response_json["error"] == "ERROR_INVALID_REFRESH_TOKEN"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_refresh_token_email_verification_required(api_client, data_fixture):
|
||||
data_fixture.create_password_provider()
|
||||
user = data_fixture.create_user(email="test@example.com", password="password")
|
||||
|
||||
# obtain refresh token
|
||||
response = api_client.post(
|
||||
reverse("api:user:token_auth"),
|
||||
{"email": "test@example.com", "password": "password"},
|
||||
format="json",
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
refresh_token = response_json["refresh_token"]
|
||||
|
||||
# change email verification setting
|
||||
settings = CoreHandler().get_settings()
|
||||
settings.email_verification = Settings.EmailVerificationOptions.ENFORCED
|
||||
settings.save()
|
||||
|
||||
profile = user.profile
|
||||
profile.email_verified = False
|
||||
profile.save()
|
||||
|
||||
# using the refresh token is not possible any more
|
||||
response = api_client.post(
|
||||
reverse("api:user:token_refresh"),
|
||||
{"refresh_token": refresh_token},
|
||||
format="json",
|
||||
)
|
||||
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_401_UNAUTHORIZED
|
||||
assert response_json["error"] == "ERROR_EMAIL_VERIFICATION_REQUIRED"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_refresh_token_email_verification_not_enforced(api_client, data_fixture):
|
||||
data_fixture.create_password_provider()
|
||||
user = data_fixture.create_user(email="test@example.com", password="password")
|
||||
|
||||
# obtain refresh token
|
||||
response = api_client.post(
|
||||
reverse("api:user:token_auth"),
|
||||
{"email": "test@example.com", "password": "password"},
|
||||
format="json",
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
refresh_token = response_json["refresh_token"]
|
||||
|
||||
# change email verification setting
|
||||
settings = CoreHandler().get_settings()
|
||||
settings.email_verification = Settings.EmailVerificationOptions.RECOMMENDED
|
||||
settings.save()
|
||||
|
||||
profile = user.profile
|
||||
profile.email_verified = False
|
||||
profile.save()
|
||||
|
||||
# using the refresh token is possible
|
||||
response = api_client.post(
|
||||
reverse("api:user:token_refresh"),
|
||||
{"refresh_token": refresh_token},
|
||||
format="json",
|
||||
)
|
||||
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_refresh_token_email_verification_not_required(api_client, data_fixture):
|
||||
user = data_fixture.create_user(email="test@example.com", password="password")
|
||||
|
||||
# obtain refresh token
|
||||
# the auth claim will not be set to password authentication
|
||||
tokens = generate_session_tokens_for_user(
|
||||
user, include_refresh_token=True, verified_email_claim=None
|
||||
)
|
||||
refresh_token = tokens["refresh_token"]
|
||||
|
||||
# change email verification setting
|
||||
settings = CoreHandler().get_settings()
|
||||
settings.email_verification = Settings.EmailVerificationOptions.ENFORCED
|
||||
settings.save()
|
||||
|
||||
profile = user.profile
|
||||
profile.email_verified = False
|
||||
profile.save()
|
||||
|
||||
# using the refresh token is possible
|
||||
response = api_client.post(
|
||||
reverse("api:user:token_refresh"),
|
||||
{"refresh_token": refresh_token},
|
||||
format="json",
|
||||
)
|
||||
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_token_verify(api_client, data_fixture):
|
||||
class TmpPlugin(Plugin):
|
||||
|
@ -384,6 +526,73 @@ def test_token_verify(api_client, data_fixture):
|
|||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_token_verify_email_verification_required(api_client, data_fixture):
|
||||
data_fixture.create_password_provider()
|
||||
user = data_fixture.create_user(email="test@example.com", password="password")
|
||||
|
||||
# obtain refresh token
|
||||
response = api_client.post(
|
||||
reverse("api:user:token_auth"),
|
||||
{"email": "test@example.com", "password": "password"},
|
||||
format="json",
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
refresh_token = response_json["refresh_token"]
|
||||
|
||||
# change email verification setting
|
||||
settings = CoreHandler().get_settings()
|
||||
settings.email_verification = Settings.EmailVerificationOptions.ENFORCED
|
||||
settings.save()
|
||||
|
||||
profile = user.profile
|
||||
profile.email_verified = False
|
||||
profile.save()
|
||||
|
||||
# using the refresh token is not possible any more
|
||||
response = api_client.post(
|
||||
reverse("api:user:token_verify"),
|
||||
{"refresh_token": refresh_token},
|
||||
format="json",
|
||||
)
|
||||
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_401_UNAUTHORIZED
|
||||
assert response_json["error"] == "ERROR_EMAIL_VERIFICATION_REQUIRED"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_token_verify_email_verification_not_required(api_client, data_fixture):
|
||||
user = data_fixture.create_user(email="test@example.com", password="password")
|
||||
|
||||
# obtain refresh token
|
||||
# the auth claim will not be set to password authentication
|
||||
tokens = generate_session_tokens_for_user(
|
||||
user, include_refresh_token=True, verified_email_claim=None
|
||||
)
|
||||
refresh_token = tokens["refresh_token"]
|
||||
|
||||
# change email verification setting
|
||||
settings = CoreHandler().get_settings()
|
||||
settings.email_verification = Settings.EmailVerificationOptions.ENFORCED
|
||||
settings.save()
|
||||
|
||||
profile = user.profile
|
||||
profile.email_verified = False
|
||||
profile.save()
|
||||
|
||||
# using the refresh token is possible
|
||||
response = api_client.post(
|
||||
reverse("api:user:token_verify"),
|
||||
{"refresh_token": refresh_token},
|
||||
format="json",
|
||||
)
|
||||
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_verify_token_is_invalidated_after_password_change(api_client, data_fixture):
|
||||
with freeze_time("2020-01-01 12:00"):
|
||||
|
|
|
@ -670,6 +670,27 @@ def test_password_reset(data_fixture, client):
|
|||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_password_reset_email_verified_email(data_fixture, client, mailoutbox):
|
||||
data_fixture.create_password_provider()
|
||||
user = data_fixture.create_user()
|
||||
handler = UserHandler()
|
||||
signer = handler.get_reset_password_signer()
|
||||
|
||||
with freeze_time("2020-01-01 12:00"):
|
||||
token = signer.dumps(user.id)
|
||||
|
||||
response = client.post(
|
||||
reverse("api:user:reset_password"),
|
||||
{"token": token, "password": "newpassword"},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 204
|
||||
|
||||
user.refresh_from_db()
|
||||
assert user.profile.email_verified is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_change_password(data_fixture, client):
|
||||
data_fixture.create_password_provider()
|
||||
|
@ -993,3 +1014,114 @@ def test_create_user_password_auth_disabled(api_client, data_fixture):
|
|||
"error": "ERROR_AUTH_PROVIDER_DISABLED",
|
||||
"detail": "Authentication provider is disabled.",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_verify_email_address(client, data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
token = UserHandler().create_email_verification_token(user)
|
||||
|
||||
response = client.post(
|
||||
reverse("api:user:verify_email"),
|
||||
{"token": token},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_204_NO_CONTENT
|
||||
user.profile.email_verified is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_verify_email_address_already_verified(client, data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
profile = user.profile
|
||||
profile.email_verified = True
|
||||
profile.save()
|
||||
token = UserHandler().create_email_verification_token(user)
|
||||
|
||||
response = client.post(
|
||||
reverse("api:user:verify_email"),
|
||||
{"token": token},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_verify_email_address_inactive_user(client, data_fixture):
|
||||
user = data_fixture.create_user(is_active=False)
|
||||
token = UserHandler().create_email_verification_token(user)
|
||||
|
||||
response = client.post(
|
||||
reverse("api:user:verify_email"),
|
||||
{"token": token},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_send_verify_email_address(client, data_fixture, mailoutbox):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
|
||||
response = client.post(
|
||||
reverse("api:user:send_verify_email"),
|
||||
{
|
||||
"email": user.email,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_204_NO_CONTENT
|
||||
assert len(mailoutbox) == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_send_verify_email_address_user_not_found(client, data_fixture, mailoutbox):
|
||||
response = client.post(
|
||||
reverse("api:user:send_verify_email"),
|
||||
{
|
||||
"email": "doesntexist@example.com",
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_204_NO_CONTENT
|
||||
assert len(mailoutbox) == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_send_verify_email_address_already_verified(client, data_fixture, mailoutbox):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
profile = user.profile
|
||||
profile.email_verified = True
|
||||
profile.save()
|
||||
|
||||
response = client.post(
|
||||
reverse("api:user:send_verify_email"),
|
||||
{
|
||||
"email": user.email,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_204_NO_CONTENT
|
||||
assert len(mailoutbox) == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_send_verify_email_address_inactive_user(client, data_fixture, mailoutbox):
|
||||
user, token = data_fixture.create_user_and_token(is_active=False)
|
||||
|
||||
response = client.post(
|
||||
reverse("api:user:send_verify_email"),
|
||||
{
|
||||
"email": user.email,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_204_NO_CONTENT
|
||||
assert len(mailoutbox) == 0
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from django.apps import apps
|
||||
|
||||
import pytest
|
||||
|
||||
from baserow.core.jobs.constants import JOB_FAILED, JOB_FINISHED, JOB_PENDING
|
||||
|
@ -16,7 +18,10 @@ def test_migration_remove_dangling_snapshots(
|
|||
|
||||
migrator.migrate(migrate_from)
|
||||
|
||||
user = data_fixture.create_user()
|
||||
user_model = apps.get_model("auth", "User")
|
||||
user = user_model()
|
||||
user.save()
|
||||
|
||||
workspace = data_fixture.create_workspace(user=user)
|
||||
database = data_fixture.create_database_application(workspace=workspace)
|
||||
snapshotted_database = data_fixture.create_database_application(workspace=workspace)
|
||||
|
@ -79,11 +84,9 @@ def test_migration_remove_dangling_snapshots(
|
|||
|
||||
assert Snapshot.objects.count() == 4
|
||||
snapshot_ids = set(Snapshot.objects.all().values_list("id", flat=True))
|
||||
assert snapshot_ids == set(
|
||||
[
|
||||
snapshot_finished.id,
|
||||
snapshot_pending.id,
|
||||
snapshot_marked_for_deletion.id,
|
||||
snapshot_failed_but_created.id,
|
||||
]
|
||||
)
|
||||
assert snapshot_ids == {
|
||||
snapshot_finished.id,
|
||||
snapshot_pending.id,
|
||||
snapshot_marked_for_deletion.id,
|
||||
snapshot_failed_but_created.id,
|
||||
}
|
||||
|
|
|
@ -6,7 +6,8 @@ from unittest.mock import MagicMock, patch
|
|||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import update_last_login
|
||||
from django.db import connections
|
||||
from django.db import connections, transaction
|
||||
from django.test import override_settings
|
||||
|
||||
import pytest
|
||||
from freezegun import freeze_time
|
||||
|
@ -33,7 +34,9 @@ from baserow.core.models import BlacklistedToken, UserLogEntry, Workspace, Works
|
|||
from baserow.core.registries import plugin_registry
|
||||
from baserow.core.user.exceptions import (
|
||||
DisabledSignupError,
|
||||
EmailAlreadyVerified,
|
||||
InvalidPassword,
|
||||
InvalidVerificationToken,
|
||||
PasswordDoesNotMatchValidation,
|
||||
RefreshTokenAlreadyBlacklisted,
|
||||
ResetPasswordDisabledError,
|
||||
|
@ -694,3 +697,111 @@ def test_user_handler_delete_user_log_entries_older_than(data_fixture):
|
|||
UserHandler().delete_user_log_entries_older_than(cutoff)
|
||||
|
||||
assert UserLogEntry.objects.count() == 2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_email_verification_token(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
user2 = data_fixture.create_user()
|
||||
user2.email = user.email
|
||||
|
||||
with freeze_time("2023-05-05"):
|
||||
signer = UserHandler()._get_email_verification_signer()
|
||||
token = UserHandler().create_email_verification_token(user)
|
||||
token_data = signer.loads(token)
|
||||
assert token_data["user_id"] == user.id
|
||||
assert token_data["email"] == user.email
|
||||
assert token_data["expires_at"] == "2023-05-06T00:00:00+00:00"
|
||||
|
||||
token2 = UserHandler().create_email_verification_token(user2)
|
||||
token_data2 = signer.loads(token2)
|
||||
assert token_data2["user_id"] == user2.id
|
||||
assert token_data2["email"] == user2.email
|
||||
assert token_data2["expires_at"] == "2023-05-06T00:00:00+00:00"
|
||||
|
||||
assert token != token2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_verify_email_address(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
token = UserHandler().create_email_verification_token(user)
|
||||
assert user.profile.email_verified is False
|
||||
|
||||
UserHandler().verify_email_address(token)
|
||||
|
||||
user.refresh_from_db()
|
||||
assert user.profile.email_verified is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_verify_email_address_expired(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
|
||||
with freeze_time("2023-05-05"):
|
||||
token = UserHandler().create_email_verification_token(user)
|
||||
|
||||
with freeze_time("2023-05-07"), pytest.raises(InvalidVerificationToken):
|
||||
UserHandler().verify_email_address(token)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_verify_email_address_user_doesnt_exist(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
token = UserHandler().create_email_verification_token(user)
|
||||
user.delete()
|
||||
|
||||
with pytest.raises(InvalidVerificationToken):
|
||||
UserHandler().verify_email_address(token)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_verify_email_address_user_different_email(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
token = UserHandler().create_email_verification_token(user)
|
||||
user.email = "newemail@example.com"
|
||||
user.save()
|
||||
|
||||
with pytest.raises(InvalidVerificationToken):
|
||||
UserHandler().verify_email_address(token)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_verify_email_address_already_verified(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
token = UserHandler().create_email_verification_token(user)
|
||||
profile = user.profile
|
||||
profile.email_verified = True
|
||||
profile.save()
|
||||
|
||||
with pytest.raises(EmailAlreadyVerified):
|
||||
UserHandler().verify_email_address(token)
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@override_settings(BASEROW_EMBEDDED_SHARE_HOSTNAME="http://test/")
|
||||
def test_send_email_pending_verification(data_fixture, mailoutbox):
|
||||
user = data_fixture.create_user()
|
||||
|
||||
with transaction.atomic():
|
||||
UserHandler().send_email_pending_verification(user)
|
||||
|
||||
assert len(mailoutbox) == 1
|
||||
email = mailoutbox[0]
|
||||
|
||||
assert email.subject == "Please confirm email"
|
||||
assert user.email in email.to
|
||||
|
||||
html_body = email.alternatives[0][0]
|
||||
f"http://test/auth/verify-email/" in html_body
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_send_email_pending_verification_already_verified(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
profile = user.profile
|
||||
profile.email_verified = True
|
||||
profile.save()
|
||||
|
||||
with pytest.raises(EmailAlreadyVerified):
|
||||
UserHandler().send_email_pending_verification(user)
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "feature",
|
||||
"message": "Add email verification options to user accounts",
|
||||
"issue_number": 2264,
|
||||
"bullet_points": [],
|
||||
"created_at": "2024-04-08"
|
||||
}
|
54
docs/patterns/emails.md
Normal file
54
docs/patterns/emails.md
Normal file
|
@ -0,0 +1,54 @@
|
|||
# Emails
|
||||
|
||||
## Creating a new email template
|
||||
|
||||
Baserow uses [MJML](https://mjml.io/) framework to define email templates. These templates have to be compiled into their HTML versions so that they can be used by Django.
|
||||
|
||||
Start by creating a new template (`*.mjml.eta`) in the core email template folder (`backend/src/baserow/core/templates/baserow`) or in the template folder of the contrib module that the template belongs to.
|
||||
|
||||
The template should be in MJML format and will typically inherit a base layout (`base.layout.eta`) common to all emails. Besides formatting itself, the template should be a normal Django template, using the Django template language, and should utilize standard Django syntax for translations.
|
||||
|
||||
The template will look something like this:
|
||||
|
||||
```html
|
||||
<% layout("../base.layout.eta") %>
|
||||
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text mj-class="title">{% trans "Title to be translated" %}</mj-text>
|
||||
<mj-text mj-class="text">
|
||||
{% blocktrans trimmed %}
|
||||
Text to be translated
|
||||
{% endblocktrans %}
|
||||
</mj-text>
|
||||
<mj-button mj-class="button" href="{{ some_link_passed_as_context_variable }}">
|
||||
{% trans "Link text to be translated" %}
|
||||
</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
```
|
||||
|
||||
If you are using the Baserow docker compose development environment started by `./dev.sh` script, the new template should be automatically compiled into its HTML version, resulting in a new file. If that's not the case, follow the instructions in `backend/email_compiler` to compile the template manually.
|
||||
|
||||
### Do
|
||||
|
||||
- Make sure the received email has both HTML and plain text version.
|
||||
- Make sure the whole content of the email template is translatable.
|
||||
- Make sure the links lead correctly to Baserow instance without hard-coding the URL.
|
||||
|
||||
### Don't
|
||||
|
||||
- Don't edit HTML email templates directly, edit the MJML templates instead and recompile them.
|
||||
|
||||
## Sending emails
|
||||
|
||||
Subclass `BaseEmailMessage` to define an email message with the correct template and all the needed parameters.
|
||||
|
||||
### Do
|
||||
|
||||
- Send emails in background Celery tasks.
|
||||
- Use `with translation.override(user.profile.language)` to send emails in the user's selected language.
|
||||
|
||||
## Testing
|
||||
|
||||
The Baserow dev environment (using the `./dev.sh` script) automatically starts an instance of [MailHog](https://github.com/mailhog/MailHog) at [http://localhost:8025/](http://localhost:8025/). You can verify that the emails are formatted and sent correctly there.
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-01-31 17:08+0000\n"
|
||||
"POT-Creation-Date: 2024-04-22 17:45+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -17,35 +17,36 @@ msgstr ""
|
|||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
#: baserow_enterprise/audit_log/job_types.py:143
|
||||
|
||||
#: baserow_enterprise/audit_log/job_types.py:37
|
||||
msgid "User Email"
|
||||
msgstr ""
|
||||
|
||||
#: baserow_enterprise/audit_log/job_types.py:144
|
||||
#: baserow_enterprise/audit_log/job_types.py:41
|
||||
msgid "User ID"
|
||||
msgstr ""
|
||||
|
||||
#: baserow_enterprise/audit_log/job_types.py:145
|
||||
#: baserow_enterprise/audit_log/job_types.py:45
|
||||
msgid "Group Name"
|
||||
msgstr ""
|
||||
|
||||
#: baserow_enterprise/audit_log/job_types.py:146
|
||||
#: baserow_enterprise/audit_log/job_types.py:49
|
||||
msgid "Group ID"
|
||||
msgstr ""
|
||||
|
||||
#: baserow_enterprise/audit_log/job_types.py:147
|
||||
#: baserow_enterprise/audit_log/job_types.py:53
|
||||
msgid "Action Type"
|
||||
msgstr ""
|
||||
|
||||
#: baserow_enterprise/audit_log/job_types.py:148
|
||||
#: baserow_enterprise/audit_log/job_types.py:57
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
|
||||
#: baserow_enterprise/audit_log/job_types.py:149
|
||||
#: baserow_enterprise/audit_log/job_types.py:61
|
||||
msgid "Timestamp"
|
||||
msgstr ""
|
||||
|
||||
#: baserow_enterprise/audit_log/job_types.py:150
|
||||
#: baserow_enterprise/audit_log/job_types.py:65
|
||||
msgid "IP Address"
|
||||
msgstr ""
|
||||
|
||||
|
@ -61,15 +62,19 @@ msgstr ""
|
|||
msgid "REDONE"
|
||||
msgstr ""
|
||||
|
||||
#: baserow_enterprise/role/actions.py:30
|
||||
msgid "Assign role"
|
||||
#: baserow_enterprise/role/actions.py:28
|
||||
msgid "Assign multiple roles"
|
||||
msgstr ""
|
||||
|
||||
#: baserow_enterprise/role/actions.py:29
|
||||
msgid "Multiple roles have been assigned"
|
||||
msgstr ""
|
||||
|
||||
#: baserow_enterprise/role/actions.py:32
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Role %(role_uid)s assigned to %(subject_type)s %(subject_id)s on "
|
||||
"%(scope_type)s %(scope_id)s"
|
||||
"Role %(role_uid)s assigned to subject type \"%(subject_type_name)s"
|
||||
"\" (%(subject_id)s) on scope type \"%(scope_type_name)s\" (%(scope_id)s)."
|
||||
msgstr ""
|
||||
|
||||
#: baserow_enterprise/teams/actions.py:21
|
||||
|
@ -86,38 +91,38 @@ msgstr ""
|
|||
msgid "Team \"%(name)s\" (%(team_id)s) created."
|
||||
msgstr ""
|
||||
|
||||
#: baserow_enterprise/teams/actions.py:101
|
||||
#: baserow_enterprise/teams/actions.py:105
|
||||
msgid "Update team"
|
||||
msgstr ""
|
||||
|
||||
#: baserow_enterprise/teams/actions.py:102
|
||||
#: baserow_enterprise/teams/actions.py:106
|
||||
#, python-format
|
||||
msgid "Team \"%(name)s\" (%(team_id)s) updated."
|
||||
msgstr ""
|
||||
|
||||
#: baserow_enterprise/teams/actions.py:193
|
||||
#: baserow_enterprise/teams/actions.py:203
|
||||
msgid "Delete team"
|
||||
msgstr ""
|
||||
|
||||
#: baserow_enterprise/teams/actions.py:194
|
||||
#: baserow_enterprise/teams/actions.py:204
|
||||
#, python-format
|
||||
msgid "Team \"%(team_name)s\" (%(team_id)s) deleted."
|
||||
msgstr ""
|
||||
|
||||
#: baserow_enterprise/teams/actions.py:240
|
||||
#: baserow_enterprise/teams/actions.py:253
|
||||
msgid "Create team subject"
|
||||
msgstr ""
|
||||
|
||||
#: baserow_enterprise/teams/actions.py:240
|
||||
#: baserow_enterprise/teams/actions.py:253
|
||||
#, python-format
|
||||
msgid "Subject (%(subject_id)s) created"
|
||||
msgstr ""
|
||||
|
||||
#: baserow_enterprise/teams/actions.py:320
|
||||
#: baserow_enterprise/teams/actions.py:338
|
||||
msgid "Delete team subject"
|
||||
msgstr ""
|
||||
|
||||
#: baserow_enterprise/teams/actions.py:321
|
||||
#: baserow_enterprise/teams/actions.py:339
|
||||
#, python-format
|
||||
msgid "Subject (%(subject_id)s) deleted"
|
||||
msgstr ""
|
||||
|
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-06-13 16:45+0000\n"
|
||||
"POT-Creation-Date: 2024-04-22 17:45+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -18,29 +18,39 @@ msgstr ""
|
|||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: src/baserow_premium/row_comments/actions.py:22
|
||||
#: src/baserow_premium/row_comments/actions.py:23
|
||||
msgid "Create row comment"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow_premium/row_comments/actions.py:23
|
||||
#: src/baserow_premium/row_comments/actions.py:24
|
||||
#, python-format
|
||||
msgid "Comment (%(comment_id)s) has been added to row (%(row_id)s)"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow_premium/row_comments/actions.py:96
|
||||
#: src/baserow_premium/row_comments/actions.py:108
|
||||
msgid "Update row comment"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow_premium/row_comments/actions.py:97
|
||||
#: src/baserow_premium/row_comments/actions.py:109
|
||||
#, python-format
|
||||
msgid "Comment (%(comment_id)s) has been updated in row (%(row_id)s)"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow_premium/row_comments/actions.py:186
|
||||
#: src/baserow_premium/row_comments/actions.py:203
|
||||
msgid "Delete row comment"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow_premium/row_comments/actions.py:187
|
||||
#: src/baserow_premium/row_comments/actions.py:204
|
||||
#, python-format
|
||||
msgid "Comment (%(comment_id)s) has been deleted from row (%(row_id)s)"
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow_premium/row_comments/notification_types.py:76
|
||||
#, python-format
|
||||
msgid "%(user)s mentioned you in row %(row_name)s in %(table_name)s."
|
||||
msgstr ""
|
||||
|
||||
#: src/baserow_premium/row_comments/notification_types.py:129
|
||||
#, python-format
|
||||
msgid "%(user)s posted a comment in row %(row_name)s in %(table_name)s."
|
||||
msgstr ""
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="box__message">
|
||||
<div class="box__message-icon">
|
||||
<i class="iconoir-mail-out"></i>
|
||||
</div>
|
||||
<p class="box__message-text">
|
||||
{{ $t('emailNotVerified.description') }}
|
||||
</p>
|
||||
<Button
|
||||
type="secondary"
|
||||
size="large"
|
||||
:disabled="resendLoading || resendSuccess"
|
||||
:loading="resendLoading"
|
||||
@click="resend(email)"
|
||||
>
|
||||
{{ $t('emailNotVerified.resend') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import resendEmailVerification from '@baserow/modules/core/mixins/resendEmailVerification'
|
||||
|
||||
export default {
|
||||
mixins: [resendEmailVerification],
|
||||
props: {
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -1,61 +1,66 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="displayHeader">
|
||||
<div class="auth__logo">
|
||||
<nuxt-link :to="{ name: 'index' }">
|
||||
<Logo />
|
||||
</nuxt-link>
|
||||
</div>
|
||||
<div class="auth__head">
|
||||
<h1 class="auth__head-title">
|
||||
{{ $t('login.title') }}
|
||||
</h1>
|
||||
<LangPicker />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="redirectByDefault && defaultRedirectUrl">
|
||||
{{ $t('login.redirecting') }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<LoginButtons
|
||||
show-border="bottom"
|
||||
:hide-if-no-buttons="loginButtonsCompact"
|
||||
:invitation="invitation"
|
||||
:original="original"
|
||||
/>
|
||||
<PasswordLogin
|
||||
v-if="!passwordLoginHidden"
|
||||
:invitation="invitation"
|
||||
@success="success"
|
||||
>
|
||||
</PasswordLogin>
|
||||
<LoginActions :invitation="invitation" :original="original">
|
||||
<li v-if="passwordLoginHidden">
|
||||
<a @click="passwordLoginHiddenIfDisabled = false">
|
||||
{{ $t('login.displayPasswordLogin') }}
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="settings.allow_reset_password && !passwordLoginHidden">
|
||||
<nuxt-link :to="{ name: 'forgot-password' }">
|
||||
{{ $t('login.forgotPassword') }}
|
||||
<EmailNotVerified v-if="displayEmailNotVerified" :email="emailToVerify">
|
||||
</EmailNotVerified>
|
||||
<template v-if="!displayEmailNotVerified">
|
||||
<div v-if="displayHeader">
|
||||
<div class="auth__logo">
|
||||
<nuxt-link :to="{ name: 'index' }">
|
||||
<Logo />
|
||||
</nuxt-link>
|
||||
</li>
|
||||
<li v-if="settings.allow_new_signups">
|
||||
<slot name="signup">
|
||||
{{ $t('login.signUpText') }}
|
||||
<nuxt-link :to="{ name: 'signup' }">
|
||||
{{ $t('login.signUp') }}
|
||||
</div>
|
||||
<div class="auth__head">
|
||||
<h1 class="auth__head-title">
|
||||
{{ $t('login.title') }}
|
||||
</h1>
|
||||
<LangPicker />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="redirectByDefault && defaultRedirectUrl">
|
||||
{{ $t('login.redirecting') }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<LoginButtons
|
||||
show-border="bottom"
|
||||
:hide-if-no-buttons="loginButtonsCompact"
|
||||
:invitation="invitation"
|
||||
:original="original"
|
||||
/>
|
||||
<PasswordLogin
|
||||
v-if="!passwordLoginHidden"
|
||||
:invitation="invitation"
|
||||
@success="success"
|
||||
@email-not-verified="emailNotVerified"
|
||||
>
|
||||
</PasswordLogin>
|
||||
<LoginActions :invitation="invitation" :original="original">
|
||||
<li v-if="passwordLoginHidden">
|
||||
<a @click="passwordLoginHiddenIfDisabled = false">
|
||||
{{ $t('login.displayPasswordLogin') }}
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="settings.allow_reset_password && !passwordLoginHidden">
|
||||
<nuxt-link :to="{ name: 'forgot-password' }">
|
||||
{{ $t('login.forgotPassword') }}
|
||||
</nuxt-link>
|
||||
</slot>
|
||||
</li>
|
||||
</LoginActions>
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="settings.allow_new_signups">
|
||||
<slot name="signup">
|
||||
{{ $t('login.signUpText') }}
|
||||
<nuxt-link :to="{ name: 'signup' }">
|
||||
{{ $t('login.signUp') }}
|
||||
</nuxt-link>
|
||||
</slot>
|
||||
</li>
|
||||
</LoginActions>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
import EmailNotVerified from '@baserow/modules/core/components/auth/EmailNotVerified.vue'
|
||||
import LoginButtons from '@baserow/modules/core/components/auth/LoginButtons'
|
||||
import LoginActions from '@baserow/modules/core/components/auth/LoginActions'
|
||||
import PasswordLogin from '@baserow/modules/core/components/auth/PasswordLogin'
|
||||
|
@ -67,7 +72,13 @@ import {
|
|||
import VueRouter from 'vue-router'
|
||||
|
||||
export default {
|
||||
components: { PasswordLogin, LoginButtons, LangPicker, LoginActions },
|
||||
components: {
|
||||
PasswordLogin,
|
||||
LoginButtons,
|
||||
LangPicker,
|
||||
LoginActions,
|
||||
EmailNotVerified,
|
||||
},
|
||||
props: {
|
||||
original: {
|
||||
type: String,
|
||||
|
@ -103,6 +114,8 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
passwordLoginHiddenIfDisabled: true,
|
||||
displayEmailNotVerified: false,
|
||||
emailToVerify: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -163,6 +176,10 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
emailNotVerified(email) {
|
||||
this.displayEmailNotVerified = true
|
||||
this.emailToVerify = email
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -139,6 +139,13 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
const emailVerified = this.$route.query.emailVerified
|
||||
if (emailVerified) {
|
||||
this.$store.dispatch('toast/info', {
|
||||
title: this.$i18n.t('verifyEmailAddress.emailVerifiedTitle'),
|
||||
message: this.$i18n.t('verifyEmailAddress.emailVerifiedDescription'),
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async login() {
|
||||
|
@ -184,6 +191,10 @@ export default {
|
|||
this.$t('clientHandler.disabledPasswordProviderTitle'),
|
||||
this.$t('clientHandler.disabledPasswordProviderMessage')
|
||||
)
|
||||
} else if (
|
||||
response.data?.error === 'ERROR_EMAIL_VERIFICATION_REQUIRED'
|
||||
) {
|
||||
this.$emit('email-not-verified', this.values.email)
|
||||
} else {
|
||||
this.showError(
|
||||
this.$t('error.incorrectCredentialTitle'),
|
||||
|
|
|
@ -225,7 +225,7 @@ export default {
|
|||
plugin.userCreated(this.account, this)
|
||||
})
|
||||
|
||||
this.$emit('success')
|
||||
this.$emit('success', { email: values.email })
|
||||
} catch (error) {
|
||||
this.loading = false
|
||||
this.handleError(error, 'signup', {
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<Alert v-if="shouldBeDisplayed" type="info-primary">
|
||||
<template #title> {{ $t('dashboardVerifyEmail.title') }}</template>
|
||||
<template #actions>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="resendLoading || resendSuccess"
|
||||
:loading="resendLoading"
|
||||
@click="resend(user.username)"
|
||||
>
|
||||
{{ $t('dashboardVerifyEmail.resendConfirmationEmail') }}
|
||||
</Button>
|
||||
</template>
|
||||
</Alert>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import { EMAIL_VERIFICATION_OPTIONS } from '@baserow/modules/core/enums'
|
||||
import resendEmailVerification from '@baserow/modules/core/mixins/resendEmailVerification'
|
||||
|
||||
export default {
|
||||
name: 'DashboardVerifyEmail',
|
||||
mixins: [resendEmailVerification],
|
||||
computed: {
|
||||
...mapState({
|
||||
user: (state) => state.auth.user,
|
||||
settings: (state) => state.settings.settings,
|
||||
refreshTokenPayload: (state) => state.auth.refreshTokenPayload,
|
||||
}),
|
||||
shouldBeDisplayed() {
|
||||
if (
|
||||
[
|
||||
EMAIL_VERIFICATION_OPTIONS.RECOMMENDED,
|
||||
EMAIL_VERIFICATION_OPTIONS.ENFORCED,
|
||||
].includes(this.settings.email_verification) &&
|
||||
this.user.email_verified === false &&
|
||||
this.refreshTokenPayload?.verified_email_claim ===
|
||||
EMAIL_VERIFICATION_OPTIONS.ENFORCED
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const emailVerified = this.$route.query.emailVerified
|
||||
if (emailVerified) {
|
||||
this.$store.dispatch('toast/info', {
|
||||
title: this.$i18n.t('verifyEmailAddress.emailVerifiedTitle'),
|
||||
message: this.$i18n.t('verifyEmailAddress.emailVerifiedDescription'),
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -1,5 +1,13 @@
|
|||
export const IMAGE_FILE_TYPES = ['image/jpeg', 'image/jpg', 'image/png']
|
||||
|
||||
// Keep these in sync with the backend options in
|
||||
// baserow.core.models.Settings.EmailVerificationOptions
|
||||
export const EMAIL_VERIFICATION_OPTIONS = {
|
||||
NO_VERIFICATION: 'no_verification',
|
||||
RECOMMENDED: 'recommended',
|
||||
ENFORCED: 'enforced',
|
||||
}
|
||||
|
||||
// Keep these in sync with the backend options in
|
||||
// baserow.core.models.UserProfile.EmailNotificationFrequencyOptions
|
||||
export const EMAIL_NOTIFICATIONS_FREQUENCY_OPTIONS = {
|
||||
|
|
|
@ -124,6 +124,23 @@
|
|||
"dashboardWorkspace": {
|
||||
"createApplication": "Create new"
|
||||
},
|
||||
"dashboardVerifyEmail": {
|
||||
"title": "Please check your mailbox and click on the link to verify your email.",
|
||||
"resendConfirmationEmail": "Resend confirmation email"
|
||||
},
|
||||
"resendEmailVerification": {
|
||||
"confirmationEmailSentTitle": "Confirmation email sent",
|
||||
"confirmationEmailSentDescription": "The confirmation email has been sent."
|
||||
},
|
||||
"verifyEmailAddress": {
|
||||
"emailVerifiedTitle": "Email verified",
|
||||
"emailVerifiedDescription": "The email address has been verified."
|
||||
},
|
||||
"emailNotVerified": {
|
||||
"title": "Email verification required",
|
||||
"description": "Please check your mailbox and click on the link to verify. Once verified, your account will be activated.",
|
||||
"resend": "Resend confirmation email"
|
||||
},
|
||||
"workspaceInvitation": {
|
||||
"title": "Invitation",
|
||||
"message": "{by} has invited you to join {workspace}.",
|
||||
|
@ -405,12 +422,17 @@
|
|||
"settingAllowNonStaffCreateWorkspaceOperationWarning": "New users will have a workspace automatically created for them where they are an Admin.",
|
||||
"settingTrackWorkspaceUsage": "Track workspace usage",
|
||||
"settingTrackWorkspaceUsageDescription": "Enables a nightly job that counts row and file usage per workspace, displayed on the premium workspace admin page.",
|
||||
"userDeletionGraceDelay": "User deletion",
|
||||
"settingUserDeletionGraceDelay": "Grace delay",
|
||||
"userSettings": "User",
|
||||
"settingUserDeletionGraceDelay": "Delete grace delay",
|
||||
"settingUserDeletionGraceDelayDescription": "This is the number of days without a login after which an account scheduled for deletion is permanently deleted.",
|
||||
"invalidAccountDeletionGraceDelay": "This value is required and must be a positive integer smaller than 32000",
|
||||
"enabled": "enabled",
|
||||
"maintenance": "Maintenance"
|
||||
"maintenance": "Maintenance",
|
||||
"emailVerification": "Email verification",
|
||||
"emailVerificationDescription": "Controls whether email addresses have to be verified.",
|
||||
"emailVerificationNoVerification": "No verification",
|
||||
"emailVerificationRecommended": "Recommended",
|
||||
"emailVerificationEnforced": "Enforced"
|
||||
},
|
||||
"formSidebar": {
|
||||
"actions": {
|
||||
|
|
37
web-frontend/modules/core/mixins/resendEmailVerification.js
Normal file
37
web-frontend/modules/core/mixins/resendEmailVerification.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import AuthService from '@baserow/modules/core/services/auth'
|
||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
resendLoading: false,
|
||||
resendSuccess: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async resend(email) {
|
||||
if (this.resendLoading) {
|
||||
return
|
||||
}
|
||||
|
||||
this.resendLoading = true
|
||||
|
||||
try {
|
||||
await AuthService(this.$client).sendVerifyEmail(email)
|
||||
this.resendSuccess = true
|
||||
this.$store.dispatch('toast/info', {
|
||||
title: this.$i18n.t(
|
||||
'resendEmailVerification.confirmationEmailSentTitle'
|
||||
),
|
||||
message: this.$i18n.t(
|
||||
'resendEmailVerification.confirmationEmailSentDescription'
|
||||
),
|
||||
})
|
||||
} catch (error) {
|
||||
notifyIf(error, 'emailVerification')
|
||||
}
|
||||
|
||||
this.resendLoading = false
|
||||
},
|
||||
},
|
||||
}
|
|
@ -130,8 +130,49 @@
|
|||
</div>
|
||||
<div class="admin-settings__group">
|
||||
<h2 class="admin-settings__group-title">
|
||||
{{ $t('settings.userDeletionGraceDelay') }}
|
||||
{{ $t('settings.userSettings') }}
|
||||
</h2>
|
||||
<div class="admin-settings__item">
|
||||
<div class="admin-settings__label">
|
||||
<div class="admin-settings__name">
|
||||
{{ $t('settings.emailVerification') }}
|
||||
</div>
|
||||
<div class="admin-settings__description">
|
||||
{{ $t('settings.emailVerificationDescription') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-settings__control">
|
||||
<Radio
|
||||
:value="EMAIL_VERIFICATION_OPTIONS.NO_VERIFICATION"
|
||||
:model-value="settings.email_verification"
|
||||
@input="updateSettings({ email_verification: $event })"
|
||||
>
|
||||
{{ $t('settings.emailVerificationNoVerification') }}
|
||||
</Radio>
|
||||
<Radio
|
||||
:value="EMAIL_VERIFICATION_OPTIONS.RECOMMENDED"
|
||||
:model-value="settings.email_verification"
|
||||
@input="
|
||||
updateSettings({
|
||||
email_verification: $event,
|
||||
})
|
||||
"
|
||||
>
|
||||
{{ $t('settings.emailVerificationRecommended') }}
|
||||
</Radio>
|
||||
<Radio
|
||||
:value="EMAIL_VERIFICATION_OPTIONS.ENFORCED"
|
||||
:model-value="settings.email_verification"
|
||||
@input="
|
||||
updateSettings({
|
||||
email_verification: $event,
|
||||
})
|
||||
"
|
||||
>
|
||||
{{ $t('settings.emailVerificationEnforced') }}
|
||||
</Radio>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-settings__item">
|
||||
<div class="admin-settings__label">
|
||||
<div class="admin-settings__name">
|
||||
|
@ -201,6 +242,8 @@ import { notifyIf } from '@baserow/modules/core/utils/error'
|
|||
import SettingsService from '@baserow/modules/core/services/settings'
|
||||
import { copyToClipboard } from '@baserow/modules/database/utils/clipboard'
|
||||
|
||||
import { EMAIL_VERIFICATION_OPTIONS } from '@baserow/modules/core/enums'
|
||||
|
||||
export default {
|
||||
layout: 'app',
|
||||
middleware: 'staff',
|
||||
|
@ -224,11 +267,19 @@ export default {
|
|||
...mapGetters({
|
||||
settings: 'settings/get',
|
||||
}),
|
||||
EMAIL_VERIFICATION_OPTIONS() {
|
||||
return EMAIL_VERIFICATION_OPTIONS
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'settings.account_deletion_grace_delay'(value) {
|
||||
this.account_deletion_grace_delay = value
|
||||
},
|
||||
account_deletion_grace_delay(value) {
|
||||
if (this.dataInitialized) {
|
||||
this.updateSettings({ account_deletion_grace_delay: value })
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.account_deletion_grace_delay =
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
:key="index"
|
||||
></component>
|
||||
</template>
|
||||
<DashboardVerifyEmail></DashboardVerifyEmail>
|
||||
<WorkspaceInvitation
|
||||
v-for="invitation in workspaceInvitations"
|
||||
:key="'invitation-' + invitation.id"
|
||||
|
@ -69,6 +70,7 @@ import DashboardWorkspace from '@baserow/modules/core/components/dashboard/Dashb
|
|||
import DashboardHelp from '@baserow/modules/core/components/dashboard/DashboardHelp'
|
||||
import DashboardNoWorkspaces from '@baserow/modules/core/components/dashboard/DashboardNoWorkspaces'
|
||||
import DashboardSidebar from '@baserow/modules/core/components/dashboard/DashboardSidebar'
|
||||
import DashboardVerifyEmail from '@baserow/modules/core/components/dashboard/DashboardVerifyEmail'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -78,6 +80,7 @@ export default {
|
|||
CreateWorkspaceModal,
|
||||
DashboardWorkspace,
|
||||
WorkspaceInvitation,
|
||||
DashboardVerifyEmail,
|
||||
},
|
||||
layout: 'app',
|
||||
/**
|
||||
|
|
|
@ -20,7 +20,11 @@ export default {
|
|||
if (store.getters['settings/get'].show_admin_signup_page === true) {
|
||||
return redirect({ name: 'signup' })
|
||||
} else if (store.getters['auth/isAuthenticated']) {
|
||||
return redirect({ name: 'dashboard' })
|
||||
const newQueryParams = {}
|
||||
if (route.query.emailVerified) {
|
||||
newQueryParams.emailVerified = true
|
||||
}
|
||||
return redirect({ name: 'dashboard', query: newQueryParams })
|
||||
}
|
||||
await store.dispatch('authProvider/fetchLoginOptions')
|
||||
return await workspaceInvitationToken.asyncData({ route, app })
|
||||
|
|
|
@ -1,59 +1,63 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="auth__logo">
|
||||
<nuxt-link :to="{ name: 'index' }">
|
||||
<Logo />
|
||||
</nuxt-link>
|
||||
</div>
|
||||
<div class="auth__head auth__head--more-margin">
|
||||
<h1 class="auth__head-title">
|
||||
{{ $t('signup.title') }}
|
||||
</h1>
|
||||
<LangPicker />
|
||||
</div>
|
||||
<template v-if="shouldShowAdminSignupPage">
|
||||
<Alert>
|
||||
<template #title>{{ $t('signup.requireFirstUser') }}</template>
|
||||
<p>{{ $t('signup.requireFirstUserMessage') }}</p></Alert
|
||||
>
|
||||
</template>
|
||||
<template v-if="!isSignupEnabled">
|
||||
<Alert type="error">
|
||||
<template #title>{{ $t('signup.disabled') }}</template>
|
||||
<p>{{ $t('signup.disabledMessage') }}</p></Alert
|
||||
>
|
||||
<nuxt-link :to="{ name: 'login' }" class="button button--full-width">
|
||||
{{ $t('action.backToLogin') }}
|
||||
</nuxt-link>
|
||||
</template>
|
||||
<template v-else>
|
||||
<PasswordRegister
|
||||
v-if="afterSignupStep < 0 && passwordLoginEnabled"
|
||||
:invitation="invitation"
|
||||
@success="next"
|
||||
>
|
||||
</PasswordRegister>
|
||||
<LoginButtons
|
||||
v-if="afterSignupStep < 0"
|
||||
show-border="top"
|
||||
:hide-if-no-buttons="true"
|
||||
:invitation="invitation"
|
||||
/>
|
||||
<LoginActions
|
||||
v-if="!shouldShowAdminSignupPage && afterSignupStep < 0"
|
||||
:invitation="invitation"
|
||||
>
|
||||
<li>
|
||||
{{ $t('signup.loginText') }}
|
||||
<nuxt-link :to="{ name: 'login' }">
|
||||
{{ $t('action.login') }}
|
||||
</nuxt-link>
|
||||
</li>
|
||||
</LoginActions>
|
||||
<component
|
||||
:is="afterSignupStepComponents[afterSignupStep]"
|
||||
@success="next"
|
||||
></component>
|
||||
<EmailNotVerified v-if="displayEmailNotVerified" :email="emailToVerify">
|
||||
</EmailNotVerified>
|
||||
<template v-if="!displayEmailNotVerified">
|
||||
<div class="auth__logo">
|
||||
<nuxt-link :to="{ name: 'index' }">
|
||||
<Logo />
|
||||
</nuxt-link>
|
||||
</div>
|
||||
<div class="auth__head auth__head--more-margin">
|
||||
<h1 class="auth__head-title">
|
||||
{{ $t('signup.title') }}
|
||||
</h1>
|
||||
<LangPicker />
|
||||
</div>
|
||||
<template v-if="shouldShowAdminSignupPage">
|
||||
<Alert>
|
||||
<template #title>{{ $t('signup.requireFirstUser') }}</template>
|
||||
<p>{{ $t('signup.requireFirstUserMessage') }}</p></Alert
|
||||
>
|
||||
</template>
|
||||
<template v-if="!isSignupEnabled">
|
||||
<Alert type="error">
|
||||
<template #title>{{ $t('signup.disabled') }}</template>
|
||||
<p>{{ $t('signup.disabledMessage') }}</p></Alert
|
||||
>
|
||||
<nuxt-link :to="{ name: 'login' }" class="button button--full-width">
|
||||
{{ $t('action.backToLogin') }}
|
||||
</nuxt-link>
|
||||
</template>
|
||||
<template v-else>
|
||||
<PasswordRegister
|
||||
v-if="afterSignupStep < 0 && passwordLoginEnabled"
|
||||
:invitation="invitation"
|
||||
@success="next"
|
||||
>
|
||||
</PasswordRegister>
|
||||
<LoginButtons
|
||||
v-if="afterSignupStep < 0"
|
||||
show-border="top"
|
||||
:hide-if-no-buttons="true"
|
||||
:invitation="invitation"
|
||||
/>
|
||||
<LoginActions
|
||||
v-if="!shouldShowAdminSignupPage && afterSignupStep < 0"
|
||||
:invitation="invitation"
|
||||
>
|
||||
<li>
|
||||
{{ $t('signup.loginText') }}
|
||||
<nuxt-link :to="{ name: 'login' }">
|
||||
{{ $t('action.login') }}
|
||||
</nuxt-link>
|
||||
</li>
|
||||
</LoginActions>
|
||||
<component
|
||||
:is="afterSignupStepComponents[afterSignupStep]"
|
||||
@success="next"
|
||||
></component>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -65,9 +69,17 @@ import LangPicker from '@baserow/modules/core/components/LangPicker'
|
|||
import LoginButtons from '@baserow/modules/core/components/auth/LoginButtons'
|
||||
import LoginActions from '@baserow/modules/core/components/auth/LoginActions'
|
||||
import workspaceInvitationToken from '@baserow/modules/core/mixins/workspaceInvitationToken'
|
||||
import { EMAIL_VERIFICATION_OPTIONS } from '@baserow/modules/core/enums'
|
||||
import EmailNotVerified from '@baserow/modules/core/components/auth/EmailNotVerified.vue'
|
||||
|
||||
export default {
|
||||
components: { PasswordRegister, LangPicker, LoginButtons, LoginActions },
|
||||
components: {
|
||||
PasswordRegister,
|
||||
LangPicker,
|
||||
LoginButtons,
|
||||
LoginActions,
|
||||
EmailNotVerified,
|
||||
},
|
||||
layout: 'login',
|
||||
async asyncData({ app, route, store, redirect }) {
|
||||
if (store.getters['auth/isAuthenticated']) {
|
||||
|
@ -79,6 +91,8 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
afterSignupStep: -1,
|
||||
displayEmailNotVerified: false,
|
||||
emailToVerify: null,
|
||||
}
|
||||
},
|
||||
head() {
|
||||
|
@ -112,9 +126,20 @@ export default {
|
|||
}),
|
||||
},
|
||||
methods: {
|
||||
next() {
|
||||
next(params) {
|
||||
if (params?.email) {
|
||||
this.emailToVerify = params.email
|
||||
}
|
||||
|
||||
if (this.afterSignupStep + 1 < this.afterSignupStepComponents.length) {
|
||||
this.afterSignupStep++
|
||||
} else if (
|
||||
this.emailToVerify &&
|
||||
this.settings.email_verification ===
|
||||
EMAIL_VERIFICATION_OPTIONS.ENFORCED &&
|
||||
!this.$route.query.workspaceInvitationToken
|
||||
) {
|
||||
this.displayEmailNotVerified = true
|
||||
} else {
|
||||
this.$nuxt.$router.push({ name: 'dashboard' }, () => {
|
||||
this.$store.dispatch('settings/hideAdminSignupPage')
|
||||
|
|
20
web-frontend/modules/core/pages/verifyEmailAddress.vue
Normal file
20
web-frontend/modules/core/pages/verifyEmailAddress.vue
Normal file
|
@ -0,0 +1,20 @@
|
|||
<script>
|
||||
import AuthService from '@baserow/modules/core/services/auth'
|
||||
|
||||
export default {
|
||||
async asyncData({ store, params, error, app, redirect }) {
|
||||
const { token } = params
|
||||
|
||||
try {
|
||||
await AuthService(app.$client).verifyEmail(token)
|
||||
} catch {
|
||||
return error({
|
||||
statusCode: 404,
|
||||
message: 'Not a valid confirmation token.',
|
||||
})
|
||||
}
|
||||
|
||||
return redirect({ name: 'login', query: { emailVerified: true } })
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -31,6 +31,11 @@ export const routes = [
|
|||
path: '/reset-password/:token',
|
||||
component: path.resolve(__dirname, 'pages/resetPassword.vue'),
|
||||
},
|
||||
{
|
||||
name: 'verify-email-address',
|
||||
path: '/verify-email-address/:token',
|
||||
component: path.resolve(__dirname, 'pages/verifyEmailAddress.vue'),
|
||||
},
|
||||
{
|
||||
name: 'dashboard',
|
||||
path: '/dashboard',
|
||||
|
|
|
@ -60,6 +60,16 @@ export default (client) => {
|
|||
new_password: newPassword,
|
||||
})
|
||||
},
|
||||
sendVerifyEmail(email) {
|
||||
return client.post(`/user/send-verify-email/`, {
|
||||
email,
|
||||
})
|
||||
},
|
||||
verifyEmail(token) {
|
||||
return client.post(`/user/verify-email/`, {
|
||||
token,
|
||||
})
|
||||
},
|
||||
dashboard() {
|
||||
return client.get('/user/dashboard/')
|
||||
},
|
||||
|
|
|
@ -14,6 +14,7 @@ export const state = () => ({
|
|||
refreshToken: null,
|
||||
tokenUpdatedAt: 0,
|
||||
tokenPayload: null,
|
||||
refreshTokenPayload: null,
|
||||
permissions: [],
|
||||
user: null,
|
||||
authenticated: false,
|
||||
|
@ -45,6 +46,9 @@ export const mutations = {
|
|||
state.refreshToken = refresh_token
|
||||
state.tokenUpdatedAt = tokenUpdatedAt || new Date().getTime()
|
||||
state.tokenPayload = jwtDecode(state.token)
|
||||
if (state.refreshToken) {
|
||||
state.refreshTokenPayload = jwtDecode(state.refreshToken)
|
||||
}
|
||||
// Global permissions annotated on the User.
|
||||
state.permissions = permissions
|
||||
/* eslint-enable camelcase */
|
||||
|
@ -72,6 +76,7 @@ export const mutations = {
|
|||
state.refreshToken = null
|
||||
state.tokenUpdatedAt = 0
|
||||
state.tokenPayload = null
|
||||
state.refreshTokenPayload = null
|
||||
state.authenticated = false
|
||||
state.permissions = []
|
||||
},
|
||||
|
@ -80,6 +85,7 @@ export const mutations = {
|
|||
state.refreshToken = null
|
||||
state.tokenUpdatedAt = 0
|
||||
state.tokenPayload = null
|
||||
state.refreshTokenPayload = null
|
||||
state.user = null
|
||||
state.authenticated = false
|
||||
state.permissions = []
|
||||
|
@ -157,8 +163,11 @@ export const actions = {
|
|||
workspaceInvitationToken,
|
||||
templateId
|
||||
)
|
||||
setToken(this.app, data.refresh_token)
|
||||
dispatch('setUserData', data)
|
||||
|
||||
if (data.refresh_token) {
|
||||
setToken(this.app, data.refresh_token)
|
||||
dispatch('setUserData', data)
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Logs off the user by removing the token as a cookie and clearing the user
|
||||
|
@ -317,6 +326,9 @@ export const getters = {
|
|||
refreshToken(state) {
|
||||
return state.refreshToken
|
||||
},
|
||||
refreshTokenPayload(state) {
|
||||
return state.refreshTokenPayload
|
||||
},
|
||||
webSocketId(state) {
|
||||
return state.webSocketId
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue