mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Add admin mailbox settings & Gmail type filter
Add mailbox config and admin UI plus Gmail attachment-type filtering. Backend: add mailbox.gmail properties to ApplicationProperties, extend GmailOAuthController/Service to accept a `types` query (builds filename query) and expose mailbox in AdminSettingsController. Frontend: add AdminMailboxSection, include nav entry and VALID_NAV_KEYS, update EmailInboxPage (UI, CSS) to support multi-select attachment type filtering and pass types to API, and add OG metadata for /settings/adminMailbox. Note: mailbox changes require server restart.
This commit is contained in:
@@ -66,6 +66,7 @@ public class ApplicationProperties {
|
||||
private AutomaticallyGenerated automaticallyGenerated = new AutomaticallyGenerated();
|
||||
|
||||
private Mail mail = new Mail();
|
||||
private Mailbox mailbox = new Mailbox();
|
||||
private Telegram telegram = new Telegram();
|
||||
|
||||
private Premium premium = new Premium();
|
||||
@@ -82,6 +83,18 @@ public class ApplicationProperties {
|
||||
private Cluster cluster = new Cluster();
|
||||
private Policies policies = new Policies();
|
||||
|
||||
@Data
|
||||
public static class Mailbox {
|
||||
private Gmail gmail = new Gmail();
|
||||
|
||||
@Data
|
||||
public static class Gmail {
|
||||
private String clientId = "";
|
||||
@ToString.Exclude private String clientSecret = "";
|
||||
private String redirectUri = "";
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PropertySource<?> dynamicYamlPropertySource(ConfigurableEnvironment environment)
|
||||
throws IOException {
|
||||
|
||||
+2
-1
@@ -103,11 +103,12 @@ public class GmailOAuthController {
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ResponseEntity<GmailOAuthService.GmailMessagePage> messages(
|
||||
@RequestParam(defaultValue = "inbox") String folder,
|
||||
@RequestParam(required = false) String types,
|
||||
@RequestParam(required = false) String pageToken,
|
||||
HttpServletRequest request)
|
||||
throws IOException, InterruptedException {
|
||||
GmailOAuthService.GmailToken token = sessionToken(request);
|
||||
return ResponseEntity.ok(gmailOAuthService.listMessages(token, folder, pageToken));
|
||||
return ResponseEntity.ok(gmailOAuthService.listMessages(token, folder, types, pageToken));
|
||||
}
|
||||
|
||||
@GetMapping("/api/v1/email/gmail/messages/{messageId}/attachments/{attachmentId}")
|
||||
|
||||
+24
-1
@@ -9,7 +9,9 @@ import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -118,7 +120,8 @@ public class GmailOAuthService {
|
||||
return connections.get(username);
|
||||
}
|
||||
|
||||
public GmailMessagePage listMessages(GmailToken token, String folder, String pageToken)
|
||||
public GmailMessagePage listMessages(
|
||||
GmailToken token, String folder, String types, String pageToken)
|
||||
throws IOException, InterruptedException {
|
||||
String label =
|
||||
switch (folder) {
|
||||
@@ -130,6 +133,7 @@ public class GmailOAuthService {
|
||||
pageToken == null || pageToken.isBlank()
|
||||
? ""
|
||||
: "&pageToken=" + URLEncoder.encode(pageToken, StandardCharsets.UTF_8);
|
||||
String typeQuery = buildAttachmentTypeQuery(types);
|
||||
JsonNode list =
|
||||
sendJson(
|
||||
token,
|
||||
@@ -137,6 +141,7 @@ public class GmailOAuthService {
|
||||
+ "/messages?labelIds="
|
||||
+ label
|
||||
+ "&maxResults=25&q=has%3Aattachment"
|
||||
+ typeQuery
|
||||
+ pageQuery);
|
||||
List<GmailMessage> messages = new ArrayList<>();
|
||||
for (JsonNode item : list.path("messages")) {
|
||||
@@ -152,6 +157,24 @@ public class GmailOAuthService {
|
||||
return new GmailMessagePage(messages, list.path("nextPageToken").asText(null));
|
||||
}
|
||||
|
||||
private String buildAttachmentTypeQuery(String types) {
|
||||
if (types == null || types.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
String filenameQuery =
|
||||
Arrays.stream(types.split(","))
|
||||
.map(String::trim)
|
||||
.map(String::toLowerCase)
|
||||
.filter(type -> type.matches("[a-z0-9]{1,10}"))
|
||||
.distinct()
|
||||
.map(type -> "filename:" + type)
|
||||
.collect(Collectors.joining(" "));
|
||||
if (filenameQuery.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
return "%20" + URLEncoder.encode("{" + filenameQuery + "}", StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public GmailAttachmentData downloadAttachment(
|
||||
GmailToken token, String messageId, String attachmentId)
|
||||
throws IOException, InterruptedException {
|
||||
|
||||
+2
@@ -665,6 +665,7 @@ public class AdminSettingsController {
|
||||
case "endpoints" -> applicationProperties.getEndpoints();
|
||||
case "metrics" -> applicationProperties.getMetrics();
|
||||
case "mail" -> applicationProperties.getMail();
|
||||
case "mailbox" -> applicationProperties.getMailbox();
|
||||
case "storage" -> applicationProperties.getStorage();
|
||||
case "premium" -> applicationProperties.getPremium();
|
||||
case "processexecutor", "processExecutor" -> applicationProperties.getProcessExecutor();
|
||||
@@ -690,6 +691,7 @@ public class AdminSettingsController {
|
||||
"endpoints",
|
||||
"metrics",
|
||||
"mail",
|
||||
"mailbox",
|
||||
"storage",
|
||||
"premium",
|
||||
"processExecutor",
|
||||
|
||||
@@ -450,6 +450,11 @@
|
||||
"title": "Admin Features Settings - Stirling PDF",
|
||||
"description": "The Free Adobe Acrobat alternative (10M+ Downloads)"
|
||||
},
|
||||
"/settings/adminMailbox": {
|
||||
"image": "/og_images/home.png",
|
||||
"title": "Admin Mailbox Settings - Stirling PDF",
|
||||
"description": "The Free Adobe Acrobat alternative (10M+ Downloads)"
|
||||
},
|
||||
"/settings/adminPlan": {
|
||||
"image": "/og_images/home.png",
|
||||
"title": "Admin Plan Settings - Stirling PDF",
|
||||
@@ -679,6 +684,7 @@
|
||||
"/settings/adminLegal": "/settings/adminLegal",
|
||||
"/settings/adminPremium": "/settings/adminPremium",
|
||||
"/settings/adminFeatures": "/settings/adminFeatures",
|
||||
"/settings/adminMailbox": "/settings/adminMailbox",
|
||||
"/settings/adminPlan": "/settings/adminPlan",
|
||||
"/settings/adminAudit": "/settings/adminAudit",
|
||||
"/settings/adminUsage": "/settings/adminUsage",
|
||||
|
||||
@@ -452,6 +452,11 @@
|
||||
"title": "Admin Features Settings - Stirling PDF",
|
||||
"description": "The Free Adobe Acrobat alternative (10M+ Downloads)"
|
||||
},
|
||||
"/settings/adminMailbox": {
|
||||
"image": "/og_images/home.png",
|
||||
"title": "Admin Mailbox Settings - Stirling PDF",
|
||||
"description": "The Free Adobe Acrobat alternative (10M+ Downloads)"
|
||||
},
|
||||
"/settings/adminPlan": {
|
||||
"image": "/og_images/home.png",
|
||||
"title": "Admin Plan Settings - Stirling PDF",
|
||||
@@ -692,6 +697,7 @@
|
||||
"/settings/adminLegal": "/settings/adminLegal",
|
||||
"/settings/adminPremium": "/settings/adminPremium",
|
||||
"/settings/adminFeatures": "/settings/adminFeatures",
|
||||
"/settings/adminMailbox": "/settings/adminMailbox",
|
||||
"/settings/adminPlan": "/settings/adminPlan",
|
||||
"/settings/adminAudit": "/settings/adminAudit",
|
||||
"/settings/adminUsage": "/settings/adminUsage",
|
||||
|
||||
@@ -26,6 +26,7 @@ export const VALID_NAV_KEYS = [
|
||||
"adminLegal",
|
||||
"adminPremium",
|
||||
"adminFeatures",
|
||||
"adminMailbox",
|
||||
"adminPlan",
|
||||
"adminAudit",
|
||||
"adminUsage",
|
||||
|
||||
@@ -223,12 +223,21 @@
|
||||
background: var(--c-bg);
|
||||
}
|
||||
.email-column-toolbar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
min-height: 5.15rem;
|
||||
min-height: 7.25rem;
|
||||
padding: 0.85rem 1rem;
|
||||
border-bottom: 1px solid var(--c-border-subtle);
|
||||
}
|
||||
.email-toolbar-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.email-column-toolbar h2 {
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
@@ -240,13 +249,13 @@
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
.email-search {
|
||||
width: min(15rem, 52%);
|
||||
width: min(15rem, 60%);
|
||||
}
|
||||
.email-type-filter {
|
||||
width: 8.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
.email-message-list {
|
||||
height: calc(100% - 5.15rem);
|
||||
height: calc(100% - 7.25rem);
|
||||
}
|
||||
.email-message-row {
|
||||
display: flex;
|
||||
@@ -577,12 +586,15 @@
|
||||
border-right: 0;
|
||||
}
|
||||
.email-column-toolbar {
|
||||
min-height: 4.75rem;
|
||||
min-height: 7rem;
|
||||
}
|
||||
.email-toolbar-top {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.email-search {
|
||||
width: 48%;
|
||||
}
|
||||
.email-message-list {
|
||||
height: calc(100% - 4.75rem);
|
||||
height: calc(100% - 7rem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
Menu,
|
||||
Modal,
|
||||
ScrollArea,
|
||||
Select,
|
||||
MultiSelect,
|
||||
TextInput,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
@@ -163,7 +163,9 @@ export default function EmailInboxPage() {
|
||||
DEMO_MESSAGES[0].id,
|
||||
);
|
||||
const [query, setQuery] = useState("");
|
||||
const [attachmentType, setAttachmentType] = useState<string | null>(null);
|
||||
const [selectedAttachmentTypes, setSelectedAttachmentTypes] = useState<
|
||||
string[]
|
||||
>([]);
|
||||
const [downloadedAttachment, setDownloadedAttachment] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
@@ -240,7 +242,7 @@ export default function EmailInboxPage() {
|
||||
}>;
|
||||
nextPageToken?: string | null;
|
||||
}>(
|
||||
`/api/v1/email/gmail/messages?folder=${selectedFolder}${pageToken ? `&pageToken=${encodeURIComponent(pageToken)}` : ""}`,
|
||||
`/api/v1/email/gmail/messages?folder=${selectedFolder}${selectedAttachmentTypes.length > 0 ? `&types=${encodeURIComponent(selectedAttachmentTypes.join(","))}` : ""}${pageToken ? `&pageToken=${encodeURIComponent(pageToken)}` : ""}`,
|
||||
);
|
||||
const mappedMessages = data.messages
|
||||
.filter((message) => message.attachments.length > 0)
|
||||
@@ -280,7 +282,12 @@ export default function EmailInboxPage() {
|
||||
setMessages([]);
|
||||
setNextPageToken(null);
|
||||
if (mailboxConfirmed) void loadMessages();
|
||||
}, [mailboxConfirmed, selectedFolder, refreshVersion]);
|
||||
}, [
|
||||
mailboxConfirmed,
|
||||
selectedFolder,
|
||||
refreshVersion,
|
||||
selectedAttachmentTypes,
|
||||
]);
|
||||
|
||||
const refreshInbox = () => {
|
||||
setMessages([]);
|
||||
@@ -315,7 +322,8 @@ export default function EmailInboxPage() {
|
||||
|
||||
const filteredMessages = useMemo(() => {
|
||||
const normalizedQuery = query.trim().toLocaleLowerCase();
|
||||
if (!normalizedQuery && !attachmentType) return messages;
|
||||
if (!normalizedQuery && selectedAttachmentTypes.length === 0)
|
||||
return messages;
|
||||
return messages.filter((message) =>
|
||||
(normalizedQuery
|
||||
? [message.sender, message.address, message.subject, message.preview]
|
||||
@@ -323,13 +331,13 @@ export default function EmailInboxPage() {
|
||||
.toLocaleLowerCase()
|
||||
.includes(normalizedQuery)
|
||||
: true) &&
|
||||
(attachmentType
|
||||
(selectedAttachmentTypes.length > 0
|
||||
? message.attachments.some(
|
||||
(attachment) => attachment.type === attachmentType,
|
||||
(attachment) => selectedAttachmentTypes.includes(attachment.type),
|
||||
)
|
||||
: true),
|
||||
);
|
||||
}, [attachmentType, messages, query]);
|
||||
}, [messages, query, selectedAttachmentTypes]);
|
||||
|
||||
const unreadMessageCount = messages.filter((message) => message.unread).length;
|
||||
|
||||
@@ -337,7 +345,9 @@ export default function EmailInboxPage() {
|
||||
filteredMessages.find((message) => message.id === selectedMessageId) ??
|
||||
filteredMessages[0];
|
||||
const selectedAttachments = selectedMessage?.attachments.filter(
|
||||
(attachment) => !attachmentType || attachment.type === attachmentType,
|
||||
(attachment) =>
|
||||
selectedAttachmentTypes.length === 0 ||
|
||||
selectedAttachmentTypes.includes(attachment.type),
|
||||
);
|
||||
|
||||
const connectAccount = async () => {
|
||||
@@ -519,32 +529,34 @@ export default function EmailInboxPage() {
|
||||
aria-label={t("email.messageList", "E-Mail-Liste")}
|
||||
>
|
||||
<div className="email-column-toolbar">
|
||||
<div>
|
||||
<h2>{t("email.inbox", "Posteingang")}</h2>
|
||||
<span>
|
||||
{filteredMessages.length} {t("email.messages", "Nachrichten")}
|
||||
</span>
|
||||
<div className="email-toolbar-top">
|
||||
<div>
|
||||
<h2>{t("email.inbox", "Posteingang")}</h2>
|
||||
<span>
|
||||
{filteredMessages.length} {t("email.messages", "Nachrichten")}
|
||||
</span>
|
||||
</div>
|
||||
<TextInput
|
||||
className="email-search"
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.currentTarget.value)}
|
||||
placeholder={t(
|
||||
"email.searchPlaceholder",
|
||||
"Nachrichten durchsuchen",
|
||||
)}
|
||||
leftSection={<SearchIcon fontSize="small" />}
|
||||
aria-label={t(
|
||||
"email.searchPlaceholder",
|
||||
"Nachrichten durchsuchen",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<TextInput
|
||||
className="email-search"
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.currentTarget.value)}
|
||||
placeholder={t(
|
||||
"email.searchPlaceholder",
|
||||
"Nachrichten durchsuchen",
|
||||
)}
|
||||
leftSection={<SearchIcon fontSize="small" />}
|
||||
aria-label={t(
|
||||
"email.searchPlaceholder",
|
||||
"Nachrichten durchsuchen",
|
||||
)}
|
||||
/>
|
||||
<Select
|
||||
<MultiSelect
|
||||
className="email-type-filter"
|
||||
clearable
|
||||
data={attachmentTypes}
|
||||
value={attachmentType}
|
||||
onChange={setAttachmentType}
|
||||
value={selectedAttachmentTypes}
|
||||
onChange={setSelectedAttachmentTypes}
|
||||
placeholder={t("email.fileTypeFilter", "Dateityp")}
|
||||
aria-label={t("email.fileTypeFilter", "Dateityp filtern")}
|
||||
/>
|
||||
|
||||
@@ -17,6 +17,7 @@ import AdminPlanSection from "@app/components/shared/config/configSections/Admin
|
||||
import AdminFeaturesSection from "@app/components/shared/config/configSections/AdminFeaturesSection";
|
||||
import AdminEndpointsSection from "@app/components/shared/config/configSections/AdminEndpointsSection";
|
||||
import AdminMcpSection from "@app/components/shared/config/configSections/AdminMcpSection";
|
||||
import AdminMailboxSection from "@app/components/shared/config/configSections/AdminMailboxSection";
|
||||
import AdminAiGeneralSection from "@app/components/shared/config/configSections/AdminAiGeneralSection";
|
||||
import AdminAiModelsSection from "@app/components/shared/config/configSections/AdminAiModelsSection";
|
||||
import AdminAiDocumentsSection from "@app/components/shared/config/configSections/AdminAiDocumentsSection";
|
||||
@@ -123,6 +124,14 @@ export const useConfigNavSections = (
|
||||
disabled: requiresLogin,
|
||||
disabledTooltip: requiresLogin ? enableLoginTooltip : undefined,
|
||||
},
|
||||
{
|
||||
key: "adminMailbox",
|
||||
label: t("settings.configuration.mailbox", "Mailbox"),
|
||||
icon: "mail-rounded",
|
||||
component: <AdminMailboxSection />,
|
||||
disabled: requiresLogin,
|
||||
disabledTooltip: requiresLogin ? enableLoginTooltip : undefined,
|
||||
},
|
||||
{
|
||||
key: "adminStorageSharing",
|
||||
label: t(
|
||||
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Stack,
|
||||
Paper,
|
||||
Text,
|
||||
TextInput,
|
||||
Loader,
|
||||
Group,
|
||||
} from "@mantine/core";
|
||||
import { alert } from "@app/components/toast";
|
||||
import RestartConfirmationModal from "@app/components/shared/config/RestartConfirmationModal";
|
||||
import { useRestartServer } from "@app/components/shared/config/useRestartServer";
|
||||
import { useAdminSettings } from "@app/hooks/useAdminSettings";
|
||||
import { useSettingsDirty } from "@app/hooks/useSettingsDirty";
|
||||
import PendingBadge from "@app/components/shared/config/PendingBadge";
|
||||
import { SettingsStickyFooter } from "@app/components/shared/config/SettingsStickyFooter";
|
||||
import EditableSecretField from "@app/components/shared/EditableSecretField";
|
||||
import { useLoginRequired } from "@app/hooks/useLoginRequired";
|
||||
|
||||
interface GmailMailboxSettings {
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
redirectUri?: string;
|
||||
}
|
||||
|
||||
interface MailboxSettingsData {
|
||||
gmail?: GmailMailboxSettings;
|
||||
}
|
||||
|
||||
export default function AdminMailboxSection() {
|
||||
const { t } = useTranslation();
|
||||
const { loginEnabled } = useLoginRequired();
|
||||
const {
|
||||
restartModalOpened,
|
||||
showRestartModal,
|
||||
closeRestartModal,
|
||||
restartServer,
|
||||
} = useRestartServer();
|
||||
const {
|
||||
settings,
|
||||
setSettings,
|
||||
loading,
|
||||
saving,
|
||||
fetchSettings,
|
||||
saveSettings,
|
||||
isFieldPending,
|
||||
} = useAdminSettings<MailboxSettingsData>({
|
||||
sectionName: "mailbox",
|
||||
saveTransformer: (current) => ({
|
||||
sectionData: {},
|
||||
deltaSettings: {
|
||||
"mailbox.gmail.clientId": current.gmail?.clientId ?? "",
|
||||
"mailbox.gmail.clientSecret": current.gmail?.clientSecret ?? "",
|
||||
"mailbox.gmail.redirectUri": current.gmail?.redirectUri ?? "",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, [fetchSettings]);
|
||||
|
||||
const { isDirty, resetToSnapshot, markSaved } = useSettingsDirty(
|
||||
settings,
|
||||
loading,
|
||||
);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await saveSettings();
|
||||
markSaved();
|
||||
showRestartModal();
|
||||
} catch (_error) {
|
||||
alert({
|
||||
alertType: "error",
|
||||
title: t("admin.error", "Error"),
|
||||
body: t("admin.settings.saveError", "Failed to save settings"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDiscard = useCallback(() => {
|
||||
setSettings(resetToSnapshot());
|
||||
}, [resetToSnapshot, setSettings]);
|
||||
|
||||
const gmail = settings.gmail ?? {};
|
||||
const updateGmail = (patch: Partial<GmailMailboxSettings>) =>
|
||||
setSettings({ ...settings, gmail: { ...gmail, ...patch } });
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Stack align="center" justify="center" h={200}>
|
||||
<Loader size="lg" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="settings-section-container">
|
||||
<Stack gap="lg" className="settings-section-content">
|
||||
<div>
|
||||
<Text fw={600} size="lg">
|
||||
{t("admin.settings.mailbox.title", "Mailbox")}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t(
|
||||
"admin.settings.mailbox.description",
|
||||
"Configure the OAuth connection used to read mailbox attachments.",
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Text fw={500} size="sm">
|
||||
Gmail OAuth
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t(
|
||||
"admin.settings.mailbox.gmail.note",
|
||||
"These values are read from settings.yml under mailbox.gmail. Changes require a server restart.",
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>Client ID</span>
|
||||
<PendingBadge show={isFieldPending("gmail.clientId")} />
|
||||
</Group>
|
||||
}
|
||||
value={gmail.clientId || ""}
|
||||
onChange={(event) =>
|
||||
updateGmail({ clientId: event.currentTarget.value })
|
||||
}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Group gap="xs" align="center" mb={4}>
|
||||
<span style={{ fontWeight: 500, fontSize: "0.875rem" }}>
|
||||
Client Secret
|
||||
</span>
|
||||
<PendingBadge show={isFieldPending("gmail.clientSecret")} />
|
||||
</Group>
|
||||
<EditableSecretField
|
||||
value={gmail.clientSecret || ""}
|
||||
onChange={(value) => updateGmail({ clientSecret: value })}
|
||||
placeholder="Google OAuth client secret"
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>Redirect URI</span>
|
||||
<PendingBadge show={isFieldPending("gmail.redirectUri")} />
|
||||
</Group>
|
||||
}
|
||||
description="Optional fixed public callback URI"
|
||||
placeholder="https://example.com/api/v1/email/gmail/callback"
|
||||
value={gmail.redirectUri || ""}
|
||||
onChange={(event) =>
|
||||
updateGmail({ redirectUri: event.currentTarget.value })
|
||||
}
|
||||
disabled={!loginEnabled}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
|
||||
<SettingsStickyFooter
|
||||
isDirty={isDirty}
|
||||
saving={saving}
|
||||
loginEnabled={loginEnabled}
|
||||
onSave={handleSave}
|
||||
onDiscard={handleDiscard}
|
||||
/>
|
||||
|
||||
<RestartConfirmationModal
|
||||
opened={restartModalOpened}
|
||||
onClose={closeRestartModal}
|
||||
onRestart={restartServer}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user