Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/admin/app/(all)/(dashboard)/email/email-config-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
EMAIL_USE_TLS: config["EMAIL_USE_TLS"],
EMAIL_USE_SSL: config["EMAIL_USE_SSL"],
EMAIL_FROM: config["EMAIL_FROM"],
ENABLE_SMTP: config["ENABLE_SMTP"],
},
});

const emailFormFields: TControllerInputFormField[] = [
{
key: "EMAIL_HOST",
Expand Down Expand Up @@ -101,7 +101,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
];

const onSubmit = async (formData: EmailFormValues) => {
const payload: Partial<EmailFormValues> = { ...formData };
const payload: Partial<EmailFormValues> = { ...formData, ENABLE_SMTP: "1" };

await updateInstanceConfigurations(payload)
.then(() =>
Expand Down
85 changes: 65 additions & 20 deletions apps/admin/app/(all)/(dashboard)/email/page.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,90 @@
"use client";

import { useEffect, useState } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
import { Loader } from "@plane/ui";
import { Loader, setToast, TOAST_TYPE, ToggleSwitch } from "@plane/ui";
// hooks
import { useInstance } from "@/hooks/store";
// components
import { InstanceEmailForm } from "./email-config-form";

const InstanceEmailPage = observer(() => {
// store
const { fetchInstanceConfigurations, formattedConfig } = useInstance();
const { fetchInstanceConfigurations, formattedConfig, disableEmail } = useInstance();

useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
const { isLoading } = useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());

const [isSubmitting, setIsSubmitting] = useState(false);
const [isSMTPEnabled, setIsSMTPEnabled] = useState(false);

const handleToggle = async () => {
if (isSMTPEnabled) {
setIsSubmitting(true);
try {
await disableEmail();
setIsSMTPEnabled(false);
setToast({
title: "Email feature disabled",
message: "Email feature has been disabled",
type: TOAST_TYPE.SUCCESS,
});
} catch (error) {
setToast({
title: "Error disabling email",
message: "Failed to disable email feature. Please try again.",
type: TOAST_TYPE.ERROR,
});
} finally {
setIsSubmitting(false);
}
return;
}
setIsSMTPEnabled(true);
};
useEffect(() => {
if (formattedConfig) {
setIsSMTPEnabled(formattedConfig.ENABLE_SMTP === "1");
}
}, [formattedConfig]);

return (
<>
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<div className="text-xl font-medium text-custom-text-100">Secure emails from your own instance</div>
<div className="text-sm font-normal text-custom-text-300">
Plane can send useful emails to you and your users from your own instance without talking to the Internet.
<div className="flex items-center justify-between gap-4 border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<div className="py-4 space-y-1 flex-shrink-0">
<div className="text-xl font-medium text-custom-text-100">Secure emails from your own instance</div>
<div className="text-sm font-normal text-custom-text-300">
Set it up below and please test your settings before you save them.&nbsp;
<span className="text-red-400">Misconfigs can lead to email bounces and errors.</span>
Plane can send useful emails to you and your users from your own instance without talking to the Internet.
<div className="text-sm font-normal text-custom-text-300">
Set it up below and please test your settings before you save them.&nbsp;
<span className="text-red-400">Misconfigs can lead to email bounces and errors.</span>
</div>
</div>
</div>
</div>
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
{formattedConfig ? (
<InstanceEmailForm config={formattedConfig} />
) : (
<Loader className="space-y-10">
<Loader.Item height="50px" width="75%" />
<Loader.Item height="50px" width="75%" />
<Loader.Item height="50px" width="40%" />
<Loader.Item height="50px" width="40%" />
<Loader.Item height="50px" width="20%" />
{isLoading ? (
<Loader>
<Loader.Item width="24px" height="16px" className="rounded-full" />
</Loader>
) : (
<ToggleSwitch value={isSMTPEnabled} onChange={handleToggle} size="sm" disabled={isSubmitting} />
)}
</div>
{isSMTPEnabled && !isLoading && (
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
{formattedConfig ? (
<InstanceEmailForm config={formattedConfig} />
) : (
<Loader className="space-y-10">
<Loader.Item height="50px" width="75%" />
<Loader.Item height="50px" width="75%" />
<Loader.Item height="50px" width="40%" />
<Loader.Item height="50px" width="40%" />
<Loader.Item height="50px" width="20%" />
</Loader>
)}
</div>
)}
</div>
</>
);
Expand Down
27 changes: 27 additions & 0 deletions apps/admin/core/store/instance.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface IInstanceStore {
fetchInstanceAdmins: () => Promise<IInstanceAdmin[] | undefined>;
fetchInstanceConfigurations: () => Promise<IInstanceConfiguration[] | undefined>;
updateInstanceConfigurations: (data: Partial<IFormattedInstanceConfiguration>) => Promise<IInstanceConfiguration[]>;
disableEmail: () => Promise<void>;
}

export class InstanceStore implements IInstanceStore {
Expand Down Expand Up @@ -187,4 +188,30 @@ export class InstanceStore implements IInstanceStore {
throw error;
}
};

disableEmail = async () => {
const instanceConfigurations = this.instanceConfigurations;
try {
runInAction(() => {
this.instanceConfigurations = this.instanceConfigurations?.map((config) => {
if (
[
"EMAIL_HOST",
"EMAIL_PORT",
"EMAIL_HOST_USER",
"EMAIL_HOST_PASSWORD",
"EMAIL_FROM",
"ENABLE_SMTP",
].includes(config.key)
)
return { ...config, value: "" };
return config;
});
});
await this.instanceService.disableEmail();
} catch (error) {
console.error("Error disabling the email");
this.instanceConfigurations = instanceConfigurations;
}
};
}
6 changes: 5 additions & 1 deletion apps/api/plane/license/api/views/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from .instance import InstanceEndpoint, SignUpScreenVisitedEndpoint


from .configuration import EmailCredentialCheckEndpoint, InstanceConfigurationEndpoint
from .configuration import (
EmailCredentialCheckEndpoint,
InstanceConfigurationEndpoint,
DisableEmailFeatureEndpoint,
)


from .admin import (
Expand Down
29 changes: 29 additions & 0 deletions apps/api/plane/license/api/views/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

# Django imports
from django.core.mail import BadHeaderError, EmailMultiAlternatives, get_connection
from django.db.models import Q, Case, When, Value

# Third party imports
from rest_framework import status
Expand Down Expand Up @@ -57,6 +58,34 @@ def patch(self, request):
return Response(serializer.data, status=status.HTTP_200_OK)


class DisableEmailFeatureEndpoint(BaseAPIView):
permission_classes = [InstanceAdminPermission]

@invalidate_cache(path="/api/instances/", user=False)
def delete(self, request):
try:
InstanceConfiguration.objects.filter(
Q(
key__in=[
"EMAIL_HOST",
"EMAIL_HOST_USER",
"EMAIL_HOST_PASSWORD",
"ENABLE_SMTP",
"EMAIL_PORT",
"EMAIL_FROM",
]
)
).update(
value=Case(When(key="ENABLE_SMTP", then=Value("0")), default=Value(""))
)
return Response(status=status.HTTP_200_OK)
except Exception:
return Response(
{"error": "Failed to disable email configuration"},
status=status.HTTP_400_BAD_REQUEST,
)


class EmailCredentialCheckEndpoint(BaseAPIView):
def post(self, request):
receiver_email = request.data.get("receiver_email", False)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ def handle(self, *args, **options):
"category": "GITLAB",
"is_encrypted": False,
},
{
"key": "ENABLE_SMTP",
"value": os.environ.get("ENABLE_SMTP", "0"),
"category": "SMTP",
"is_encrypted": False,
},
{
"key": "GITLAB_CLIENT_SECRET",
"value": os.environ.get("GITLAB_CLIENT_SECRET"),
Expand Down
6 changes: 6 additions & 0 deletions apps/api/plane/license/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
InstanceAdminSignInEndpoint,
InstanceAdminSignUpEndpoint,
InstanceConfigurationEndpoint,
DisableEmailFeatureEndpoint,
InstanceEndpoint,
SignUpScreenVisitedEndpoint,
InstanceAdminUserMeEndpoint,
Expand Down Expand Up @@ -35,6 +36,11 @@
InstanceConfigurationEndpoint.as_view(),
name="instance-configuration",
),
path(
"configurations/disable-email-feature/",
DisableEmailFeatureEndpoint.as_view(),
name="disable-email-configuration",
),
path(
"admins/sign-in/",
InstanceAdminSignInEndpoint.as_view(),
Expand Down
13 changes: 13 additions & 0 deletions packages/services/src/instance/instance.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,17 @@ export class InstanceService extends APIService {
throw error?.response?.data;
});
}

/**
* Disables the email configuration
* @returns {Promise<void>} Promise resolving to void
* @throws {Error} If the API request fails
*/
async disableEmail(): Promise<void> {
return this.delete("/api/instances/configurations/disable-email-feature/")
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
}
3 changes: 2 additions & 1 deletion packages/types/src/instance/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export type TInstanceEmailConfigurationKeys =
| "EMAIL_HOST_PASSWORD"
| "EMAIL_USE_TLS"
| "EMAIL_USE_SSL"
| "EMAIL_FROM";
| "EMAIL_FROM"
| "ENABLE_SMTP";