From 15e4abd68fda60b6e6d9e2f9203f25eee3209794 Mon Sep 17 00:00:00 2001 From: martinzerty Date: Fri, 14 Nov 2025 16:23:14 +0100 Subject: [PATCH 1/3] add resend email in dataview component --- .../components/DetailView/DetailView.svelte | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/frontend/src/lib/components/DetailView/DetailView.svelte b/frontend/src/lib/components/DetailView/DetailView.svelte index defecb3a04..da20d6b6a6 100644 --- a/frontend/src/lib/components/DetailView/DetailView.svelte +++ b/frontend/src/lib/components/DetailView/DetailView.svelte @@ -227,6 +227,27 @@ modalStore.trigger(modal); } + function modalSendInvitation(id: string, name: string, action: string): void { + const modalComponent: ModalComponent = { + ref: ConfirmModal, + props: { + _form: { id: id, urlmodel: getModelInfo('representatives').urlModel }, + id: id, + debug: false, + URLModel: getModelInfo('representatives').urlModel, + formAction: action + } + }; + const modal: ModalSettings = { + type: 'component', + component: modalComponent, + // Data + title: m.confirmModalTitle(), + body: `${m.confirmModalMessage()}: ${name}?` + }; + modalStore.trigger(modal); + } + function getReverseForeignKeyEndpoint({ parentModel, targetUrlModel, @@ -632,6 +653,16 @@ {/if} {#if displayEditButton()} + {#if data.urlModel === 'representatives'} + + {/if} {#if data.data.state === 'Created'} Date: Mon, 17 Nov 2025 16:56:41 +0100 Subject: [PATCH 2/3] feat: add resend invitation button and actualy send the invitation mail --- backend/iam/urls.py | 2 ++ backend/iam/views.py | 27 ++++++++++++++++++- .../components/DetailView/DetailView.svelte | 13 ++++++--- .../lib/components/Modals/ConfirmModal.svelte | 3 +++ 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/backend/iam/urls.py b/backend/iam/urls.py index 33e795c9f9..e653ccf263 100644 --- a/backend/iam/urls.py +++ b/backend/iam/urls.py @@ -9,6 +9,7 @@ LoginView, PasswordResetView, ResetPasswordConfirmView, + SendInvtationView, SessionTokenView, SetPasswordView, ) @@ -25,6 +26,7 @@ ResetPasswordConfirmView.as_view(), name="password-reset-confirm", ), + path("send-invitation/", SendInvtationView.as_view(), name="send-invitation"), path("set-password/", SetPasswordView.as_view(), name="set-password"), path("sso/", include("iam.sso.urls")), path( diff --git a/backend/iam/views.py b/backend/iam/views.py index 9b83b678c8..849a1f12a4 100644 --- a/backend/iam/views.py +++ b/backend/iam/views.py @@ -239,7 +239,7 @@ def get(self, request) -> Response: "preferences": request.user.preferences, } return Response(res_data, status=HTTP_200_OK) - + class SessionTokenView(views.APIView): """ @@ -266,6 +266,31 @@ def post(self, request): session_token = request.session.session_key return Response({"token": session_token}) +class SendInvtationView(views.APIView): + permission_classes = [permissions.AllowAny] + + @method_decorator(ensure_csrf_cookie) + def post(self, request): + email = request.data["email"] # type: ignore + associated_user = User.objects.filter(email=email).first() + if EMAIL_HOST or EMAIL_HOST_RESCUE: + if associated_user is not None and associated_user.is_local: + try: + associated_user.mailing( + email_template_name="registration/first_connection_email.html", + subject=_("CISO Assistant: Invitation"), + ) + print("Sending reset mail to", email) + except Exception as e: + print(e) + return Response(status=HTTP_202_ACCEPTED) + return Response( + data={ + "error": "Email server not configured, please contact your administrator" + }, + status=HTTP_500_INTERNAL_SERVER_ERROR, + ) + class PasswordResetView(views.APIView): permission_classes = [permissions.AllowAny] diff --git a/frontend/src/lib/components/DetailView/DetailView.svelte b/frontend/src/lib/components/DetailView/DetailView.svelte index da20d6b6a6..ef39869cfa 100644 --- a/frontend/src/lib/components/DetailView/DetailView.svelte +++ b/frontend/src/lib/components/DetailView/DetailView.svelte @@ -227,11 +227,11 @@ modalStore.trigger(modal); } - function modalSendInvitation(id: string, name: string, action: string): void { + function modalSendInvitation(id: string, name: string, action: string, email?: string): void { const modalComponent: ModalComponent = { ref: ConfirmModal, props: { - _form: { id: id, urlmodel: getModelInfo('representatives').urlModel }, + _form: { id: id, urlmodel: getModelInfo('representatives').urlModel, email: email }, id: id, debug: false, URLModel: getModelInfo('representatives').urlModel, @@ -241,7 +241,6 @@ const modal: ModalSettings = { type: 'component', component: modalComponent, - // Data title: m.confirmModalTitle(), body: `${m.confirmModalMessage()}: ${name}?` }; @@ -656,7 +655,13 @@ {#if data.urlModel === 'representatives'} + {#if _form && _form.email} + + {/if} From 9e362e7f00eb6d6bb56047d414658733d2ea28e0 Mon Sep 17 00:00:00 2001 From: martinzerty Date: Tue, 18 Nov 2025 15:37:47 +0100 Subject: [PATCH 3/3] feat: send invitation to representatives how has user attached to it --- backend/iam/views.py | 10 ++++- .../components/DetailView/DetailView.svelte | 11 +++--- .../lib/components/Modals/ConfirmModal.svelte | 3 -- .../send-invitation/+server.ts | 37 +++++++++++++++++++ 4 files changed, 50 insertions(+), 11 deletions(-) create mode 100644 frontend/src/routes/(app)/(third-party)/representatives/send-invitation/+server.ts diff --git a/backend/iam/views.py b/backend/iam/views.py index 849a1f12a4..a6e19ca944 100644 --- a/backend/iam/views.py +++ b/backend/iam/views.py @@ -239,7 +239,7 @@ def get(self, request) -> Response: "preferences": request.user.preferences, } return Response(res_data, status=HTTP_200_OK) - + class SessionTokenView(views.APIView): """ @@ -266,6 +266,7 @@ def post(self, request): session_token = request.session.session_key return Response({"token": session_token}) + class SendInvtationView(views.APIView): permission_classes = [permissions.AllowAny] @@ -273,6 +274,11 @@ class SendInvtationView(views.APIView): def post(self, request): email = request.data["email"] # type: ignore associated_user = User.objects.filter(email=email).first() + if associated_user is None: + return Response( + status=HTTP_500_INTERNAL_SERVER_ERROR, + data={"error": "No user associated with this email"}, + ) if EMAIL_HOST or EMAIL_HOST_RESCUE: if associated_user is not None and associated_user.is_local: try: @@ -280,7 +286,7 @@ def post(self, request): email_template_name="registration/first_connection_email.html", subject=_("CISO Assistant: Invitation"), ) - print("Sending reset mail to", email) + print("Sending invitation mail to", email) except Exception as e: print(e) return Response(status=HTTP_202_ACCEPTED) diff --git a/frontend/src/lib/components/DetailView/DetailView.svelte b/frontend/src/lib/components/DetailView/DetailView.svelte index ef39869cfa..3e4fdd6996 100644 --- a/frontend/src/lib/components/DetailView/DetailView.svelte +++ b/frontend/src/lib/components/DetailView/DetailView.svelte @@ -227,7 +227,7 @@ modalStore.trigger(modal); } - function modalSendInvitation(id: string, name: string, action: string, email?: string): void { + function modalSendInvitation(id: string, email: string, action: string): void { const modalComponent: ModalComponent = { ref: ConfirmModal, props: { @@ -242,7 +242,7 @@ type: 'component', component: modalComponent, title: m.confirmModalTitle(), - body: `${m.confirmModalMessage()}: ${name}?` + body: `Do you want to send the invitation to ${email}?` }; modalStore.trigger(modal); } @@ -652,15 +652,14 @@ {/if} {#if displayEditButton()} - {#if data.urlModel === 'representatives'} + {#if data.urlModel === 'representatives' && data.data.user} - {#if _form && _form.email} - - {/if} diff --git a/frontend/src/routes/(app)/(third-party)/representatives/send-invitation/+server.ts b/frontend/src/routes/(app)/(third-party)/representatives/send-invitation/+server.ts new file mode 100644 index 0000000000..6310669448 --- /dev/null +++ b/frontend/src/routes/(app)/(third-party)/representatives/send-invitation/+server.ts @@ -0,0 +1,37 @@ +import { BASE_API_URL } from '$lib/utils/constants'; +import type { RequestHandler } from './$types'; + +export const POST: RequestHandler = async (event) => { + const requestInitOptions: RequestInit = { + method: 'POST' + }; + const form = await event.request.formData(); + const raw = form.get('__superform_json') as string; + + let parsed = JSON.parse(raw); + + let email: string | undefined; + + if (Array.isArray(parsed)) { + const mapping = parsed[0]; + if (mapping && typeof mapping === 'object' && typeof mapping.email === 'number') { + const emailIndex = mapping.email; + email = parsed[emailIndex]; + } else { + email = parsed.find((v: any) => typeof v === 'string' && v.includes('@')); + } + } else if (parsed && typeof parsed === 'object') { + email = parsed.email ?? parsed['__email'] ?? undefined; + } + + const res = await fetch(`${BASE_API_URL}/iam/send-invitation/`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: email }) + }); + return new Response(null, { + headers: { + 'Content-Type': 'application/json' + } + }); +};