|
57 | 57 | from baserow.core.handler import CoreHandler |
58 | 58 | from baserow.core.models import Settings, Template, WorkspaceInvitation |
59 | 59 | from baserow.core.user.actions import ( |
| 60 | + ChangeEmailActionType, |
60 | 61 | ChangeUserPasswordActionType, |
61 | 62 | CreateUserActionType, |
62 | 63 | ResetUserPasswordActionType, |
63 | 64 | ScheduleUserDeletionActionType, |
| 65 | + SendChangeEmailConfirmationActionType, |
64 | 66 | SendResetUserPasswordActionType, |
65 | 67 | SendVerifyEmailAddressActionType, |
66 | 68 | UpdateUserActionType, |
67 | 69 | VerifyEmailAddressActionType, |
68 | 70 | ) |
69 | 71 | from baserow.core.user.exceptions import ( |
| 72 | + ChangeEmailNotAllowed, |
70 | 73 | DeactivatedUserException, |
71 | 74 | DisabledSignupError, |
| 75 | + EmailAlreadyChanged, |
72 | 76 | EmailAlreadyVerified, |
73 | 77 | InvalidPassword, |
74 | 78 | InvalidVerificationToken, |
|
84 | 88 | from .errors import ( |
85 | 89 | ERROR_ALREADY_EXISTS, |
86 | 90 | ERROR_AUTH_PROVIDER_DISABLED, |
| 91 | + ERROR_CHANGE_EMAIL_NOT_ALLOWED, |
87 | 92 | ERROR_CLIENT_SESSION_ID_HEADER_NOT_SET, |
88 | 93 | ERROR_DEACTIVATED_USER, |
89 | 94 | ERROR_DISABLED_RESET_PASSWORD, |
90 | 95 | ERROR_DISABLED_SIGNUP, |
| 96 | + ERROR_EMAIL_ALREADY_CHANGED, |
91 | 97 | ERROR_EMAIL_ALREADY_VERIFIED, |
92 | 98 | ERROR_EMAIL_VERIFICATION_REQUIRED, |
93 | 99 | ERROR_INVALID_CREDENTIALS, |
|
107 | 113 | ) |
108 | 114 | from .serializers import ( |
109 | 115 | AccountSerializer, |
| 116 | + ChangeEmailSerializer, |
110 | 117 | ChangePasswordBodyValidationSerializer, |
111 | 118 | DashboardSerializer, |
112 | 119 | RegisterSerializer, |
113 | 120 | ResetPasswordBodyValidationSerializer, |
| 121 | + SendChangeEmailConfirmationSerializer, |
114 | 122 | SendResetPasswordEmailBodyValidationSerializer, |
115 | 123 | SendVerifyEmailAddressSerializer, |
116 | 124 | ShareOnboardingDetailsWithBaserowSerializer, |
@@ -482,6 +490,104 @@ def post(self, request, data): |
482 | 490 | return Response(status=204) |
483 | 491 |
|
484 | 492 |
|
| 493 | +class SendChangeEmailConfirmationView(APIView): |
| 494 | + permission_classes = (IsAuthenticated,) |
| 495 | + |
| 496 | + @extend_schema( |
| 497 | + tags=["User"], |
| 498 | + request=SendChangeEmailConfirmationSerializer, |
| 499 | + operation_id="send_change_email_confirmation", |
| 500 | + description=( |
| 501 | + "Sends an email to the new email address containing a confirmation link. " |
| 502 | + "The user must provide their current password to initiate this request. " |
| 503 | + f"The link is going to be valid for " |
| 504 | + f"{int(settings.CHANGE_EMAIL_TOKEN_MAX_AGE / 60 / 60)} hours." |
| 505 | + ), |
| 506 | + responses={ |
| 507 | + 204: None, |
| 508 | + 400: get_error_schema( |
| 509 | + [ |
| 510 | + "ERROR_REQUEST_BODY_VALIDATION", |
| 511 | + "ERROR_HOSTNAME_IS_NOT_ALLOWED", |
| 512 | + "ERROR_INVALID_OLD_PASSWORD", |
| 513 | + "ERROR_ALREADY_EXISTS", |
| 514 | + "ERROR_CHANGE_EMAIL_NOT_ALLOWED", |
| 515 | + "ERROR_AUTH_PROVIDER_DISABLED", |
| 516 | + ] |
| 517 | + ), |
| 518 | + }, |
| 519 | + ) |
| 520 | + @transaction.atomic |
| 521 | + @map_exceptions( |
| 522 | + { |
| 523 | + BaseURLHostnameNotAllowed: ERROR_HOSTNAME_IS_NOT_ALLOWED, |
| 524 | + InvalidPassword: ERROR_INVALID_OLD_PASSWORD, |
| 525 | + UserAlreadyExist: ERROR_ALREADY_EXISTS, |
| 526 | + AuthProviderDisabled: ERROR_AUTH_PROVIDER_DISABLED, |
| 527 | + ChangeEmailNotAllowed: ERROR_CHANGE_EMAIL_NOT_ALLOWED, |
| 528 | + } |
| 529 | + ) |
| 530 | + @validate_body(SendChangeEmailConfirmationSerializer) |
| 531 | + def post(self, request, data): |
| 532 | + """ |
| 533 | + Sends a confirmation email to the new email address if the password is correct. |
| 534 | + """ |
| 535 | + |
| 536 | + action_type_registry.get(SendChangeEmailConfirmationActionType.type).do( |
| 537 | + request.user, data["new_email"], data["password"], data["base_url"] |
| 538 | + ) |
| 539 | + |
| 540 | + return Response(status=204) |
| 541 | + |
| 542 | + |
| 543 | +class ChangeEmailView(APIView): |
| 544 | + permission_classes = (AllowAny,) |
| 545 | + |
| 546 | + @extend_schema( |
| 547 | + tags=["User"], |
| 548 | + request=ChangeEmailSerializer, |
| 549 | + operation_id="change_email", |
| 550 | + description=( |
| 551 | + "Changes the email address of a user if the confirmation token is valid. " |
| 552 | + "The **send_change_email_confirmation** endpoint sends an email to the " |
| 553 | + "new address containing the token. That token can be used to change the " |
| 554 | + "email address here." |
| 555 | + ), |
| 556 | + responses={ |
| 557 | + 204: None, |
| 558 | + 400: get_error_schema( |
| 559 | + [ |
| 560 | + "BAD_TOKEN_SIGNATURE", |
| 561 | + "EXPIRED_TOKEN_SIGNATURE", |
| 562 | + "ERROR_USER_NOT_FOUND", |
| 563 | + "ERROR_ALREADY_EXISTS", |
| 564 | + "ERROR_EMAIL_ALREADY_CHANGED", |
| 565 | + "ERROR_REQUEST_BODY_VALIDATION", |
| 566 | + ] |
| 567 | + ), |
| 568 | + }, |
| 569 | + auth=[], |
| 570 | + ) |
| 571 | + @transaction.atomic |
| 572 | + @map_exceptions( |
| 573 | + { |
| 574 | + BadSignature: BAD_TOKEN_SIGNATURE, |
| 575 | + BadTimeSignature: BAD_TOKEN_SIGNATURE, |
| 576 | + SignatureExpired: EXPIRED_TOKEN_SIGNATURE, |
| 577 | + UserNotFound: ERROR_USER_NOT_FOUND, |
| 578 | + UserAlreadyExist: ERROR_ALREADY_EXISTS, |
| 579 | + EmailAlreadyChanged: ERROR_EMAIL_ALREADY_CHANGED, |
| 580 | + } |
| 581 | + ) |
| 582 | + @validate_body(ChangeEmailSerializer) |
| 583 | + def post(self, request, data): |
| 584 | + """Changes the user's email address if the provided token is valid.""" |
| 585 | + |
| 586 | + action_type_registry.get(ChangeEmailActionType.type).do(data["token"]) |
| 587 | + |
| 588 | + return Response(status=204) |
| 589 | + |
| 590 | + |
485 | 591 | class AccountView(APIView): |
486 | 592 | permission_classes = (IsAuthenticated,) |
487 | 593 |
|
|
0 commit comments