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..a6e19ca944 100644
--- a/backend/iam/views.py
+++ b/backend/iam/views.py
@@ -267,6 +267,37 @@ def post(self, request):
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 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:
+ associated_user.mailing(
+ email_template_name="registration/first_connection_email.html",
+ subject=_("CISO Assistant: Invitation"),
+ )
+ print("Sending invitation 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 defecb3a04..3e4fdd6996 100644
--- a/frontend/src/lib/components/DetailView/DetailView.svelte
+++ b/frontend/src/lib/components/DetailView/DetailView.svelte
@@ -227,6 +227,26 @@
modalStore.trigger(modal);
}
+ function modalSendInvitation(id: string, email: string, action: string): void {
+ const modalComponent: ModalComponent = {
+ ref: ConfirmModal,
+ props: {
+ _form: { id: id, urlmodel: getModelInfo('representatives').urlModel, email: email },
+ id: id,
+ debug: false,
+ URLModel: getModelInfo('representatives').urlModel,
+ formAction: action
+ }
+ };
+ const modal: ModalSettings = {
+ type: 'component',
+ component: modalComponent,
+ title: m.confirmModalTitle(),
+ body: `Do you want to send the invitation to ${email}?`
+ };
+ modalStore.trigger(modal);
+ }
+
function getReverseForeignKeyEndpoint({
parentModel,
targetUrlModel,
@@ -632,6 +652,21 @@
{/if}
{#if displayEditButton()}
+ {#if data.urlModel === 'representatives' && data.data.user}
+
+ {/if}
{#if data.data.state === 'Created'}
{
+ 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'
+ }
+ });
+};