diff --git a/django_project_base/account/rest/profile.py b/django_project_base/account/rest/profile.py
index d4d4235d..47696565 100644
--- a/django_project_base/account/rest/profile.py
+++ b/django_project_base/account/rest/profile.py
@@ -1,4 +1,5 @@
import datetime
+from random import randrange
import django
import swapper
@@ -19,6 +20,7 @@
from dynamicforms.template_render.layout import Column, Layout, Row
from dynamicforms.template_render.responsive_table_layout import ResponsiveTableLayout, ResponsiveTableLayouts
from dynamicforms.viewsets import ModelViewSet
+from natural.date import compress
from rest_framework import exceptions, filters, status
from rest_framework.decorators import action
from rest_framework.exceptions import APIException, ValidationError
@@ -34,7 +36,7 @@
from django_project_base.account.rest.project_profiles_utils import get_project_members
from django_project_base.base.event import UserRegisteredEvent
from django_project_base.constants import NOTIFY_NEW_USER_SETTING_NAME
-from django_project_base.notifications.email_notification import EMailNotification
+from django_project_base.notifications.email_notification import EMailNotification, EMailNotificationWithListOfEmails
from django_project_base.notifications.models import DjangoProjectBaseMessage
from django_project_base.permissions import BasePermissions
from django_project_base.rest.project import ProjectSerializer, ProjectViewSet
@@ -417,10 +419,56 @@ def get_current_profile(self, request: Request, **kwargs) -> Response:
@get_current_profile.mapping.post
def update_current_profile(self, request: Request, **kwargs) -> Response:
user: Model = request.user
+ new_email = request.data.pop("email", None)
serializer = self.get_serializer(user, data=request.data, many=False)
serializer.is_valid(raise_exception=True)
serializer.save()
- return Response(serializer.data)
+ email_changed = new_email and user.email != new_email
+ response = Response(serializer.data)
+ if email_changed:
+ code = randrange(100001, 999999)
+ response.set_cookie("verify-email", user.pk, samesite="Lax")
+ request.session[f"email-changed-{code}-{user.pk}"] = new_email
+ # TODO: Use system email
+ # TODO: SEND THIS AS SYSTEM MSG WHEN PR IS MERGED
+ # TODO: https://taiga.velis.si/project/velis-django-project-admin/issue/728
+ EMailNotificationWithListOfEmails(
+ message=DjangoProjectBaseMessage(
+ subject=f"{_('Email change for account on')} {request.META['HTTP_HOST']}",
+ body=f"{_('You requested an email change for your account at')} {request.META['HTTP_HOST']}. "
+ f"\n\n{_('Your verification code is')}: "
+ f"{code} \n\n {_('Code is valid for')} {compress(settings.CONFIRMATION_CODE_TIMEOUT)}.\n",
+ footer="",
+ content_type=DjangoProjectBaseMessage.PLAIN_TEXT,
+ ),
+ recipients=[new_email],
+ project=self.request.selected_project.slug,
+ user=user.pk,
+ ).send()
+ return response
+
+ @extend_schema(exclude=True)
+ @action(
+ methods=["POST"],
+ detail=False,
+ url_path="confirm-new-email",
+ url_name="confirm-new-email",
+ )
+ @transaction.atomic()
+ def confirm_new_email(self, request: Request, **kwargs) -> Response:
+ user = request.user
+ if not request.data.get("code"):
+ raise ValidationError(dict(code=[_("Code required")]))
+ key = f"email-changed-{request.data['code']}-{user.pk}"
+ new_email = request.session.get(key)
+ if email := new_email:
+ user.email = email
+ user.save(update_fields=["email"])
+ request.session.pop(key, None)
+ response = Response()
+ response.delete_cookie("verify-email")
+ return response
+ raise ValidationError(dict(code=[_("Invalid code")]))
@extend_schema(
description="Marks profile of calling user for deletion in future. Future date is determined " "by settings",
diff --git a/vue/components/profile-search.vue b/vue/components/profile-search.vue
index d7ff627f..675356af 100644
--- a/vue/components/profile-search.vue
+++ b/vue/components/profile-search.vue
@@ -91,9 +91,12 @@ function customLabel(profile: UserDataJSON) {
@select="onSelect"
>
- {{ props.option.email }}
- {{ props.option.first_name }}
- {{ props.option.last_name }}
+ {{ //@ts-ignore
+ props.option.email }}
+ {{ //@ts-ignore
+ props.option.first_name }}
+ {{ //@ts-ignore
+ props.option.last_name }}
diff --git a/vue/components/user-session/login-dialog.vue b/vue/components/user-session/login-dialog.vue
index 9c2345ec..e78ab395 100644
--- a/vue/components/user-session/login-dialog.vue
+++ b/vue/components/user-session/login-dialog.vue
@@ -7,7 +7,7 @@ import {
} from '@velis/dynamicforms';
import { ref, watch } from 'vue';
-import useLogin from './login';
+import { useLogin } from './login';
import SocialLogos from './social-logos.vue';
import { showLoginDialog } from './use-login-dialog';
// TODO: needs to be moved to /rest/about or to some configuration. definitely needs to be app-specific
diff --git a/vue/components/user-session/login-inline.vue b/vue/components/user-session/login-inline.vue
index 9095e8ab..0c2f5d82 100644
--- a/vue/components/user-session/login-inline.vue
+++ b/vue/components/user-session/login-inline.vue
@@ -46,7 +46,7 @@ import _ from 'lodash';
import { onMounted } from 'vue';
import { useCookies } from 'vue3-cookies';
-import useLogin from './login';
+import { useLogin } from './login';
import SocialLogos from './social-logos.vue';
import useUserSessionStore from './state';
diff --git a/vue/components/user-session/login.ts b/vue/components/user-session/login.ts
index 5bb13117..224691e0 100644
--- a/vue/components/user-session/login.ts
+++ b/vue/components/user-session/login.ts
@@ -25,21 +25,21 @@ const resetPasswordErrors = reactive({} as { [key: string]: any[] });
let resetPasswordData = { user_id: 0, timestamp: 0, signature: '' };
-export default function useLogin() {
+function parseErrors(apiErr: AxiosError, errsStore: { [key: string]: any[] }) {
+ Object.keys(errsStore).forEach((key: string) => {
+ delete errsStore[key];
+ });
+ if (apiErr.response?.data?.detail) {
+ errsStore.non_field_errors = [apiErr.response.data.detail];
+ } else {
+ Object.assign(errsStore, apiErr.response?.data);
+ }
+}
+
+function useLogin() {
const userSession = useUserSessionStore();
const loginConsumer = new ConsumerLogicApi(userSession.apiEndpointLogin);
- function parseErrors(apiErr: AxiosError, errsStore: { [key: string]: any[] }) {
- Object.keys(errsStore).forEach((key: string) => {
- delete errsStore[key];
- });
- if (apiErr.response?.data?.detail) {
- errsStore.non_field_errors = [apiErr.response.data.detail];
- } else {
- Object.assign(errsStore, apiErr.response?.data);
- }
- }
-
async function enterResetPasswordData() {
// eslint-disable-next-line vue/max-len
if (_.includes(window.location.hash, '#reset-user-password') || _.includes(window.location.hash, '#/reset-user-password')) {
@@ -243,3 +243,5 @@ export default function useLogin() {
openRegistration,
};
}
+
+export { useLogin, parseErrors };
diff --git a/vue/components/user-session/user-profile.vue b/vue/components/user-session/user-profile.vue
index e64b8f50..74ae5363 100644
--- a/vue/components/user-session/user-profile.vue
+++ b/vue/components/user-session/user-profile.vue
@@ -1,8 +1,9 @@