fix(calendar): SSRF exception for Exchange + error messages + domain in edit
- SSRF check skipped for Exchange type (internal EWS servers are common)
- testConnectionFromConfig catches SSRF/validation errors, returns {success:false,error} instead of throwing 403
- updateSource reads existing.type to determine effective type for SSRF check
- Panel shows saveError/editSaveError on failed add/update
- Edit form initialValues now includes domain field
- i18n: calendar.saveError key added (de+en)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -141,7 +141,8 @@ export class CalendarService {
|
|||||||
* T-05-11: Validates URL against private IP ranges (SSRF).
|
* T-05-11: Validates URL against private IP ranges (SSRF).
|
||||||
*/
|
*/
|
||||||
async addSource(userId: string, tenantId: string, dto: CreateCalendarSourceDto) {
|
async addSource(userId: string, tenantId: string, dto: CreateCalendarSourceDto) {
|
||||||
await this.validateUrlNotPrivate(dto.url);
|
// Exchange EWS servers are commonly on internal networks — SSRF check skipped for exchange type
|
||||||
|
if (dto.type !== 'exchange') await this.validateUrlNotPrivate(dto.url);
|
||||||
|
|
||||||
const data: Record<string, unknown> = {
|
const data: Record<string, unknown> = {
|
||||||
userId,
|
userId,
|
||||||
@@ -174,7 +175,7 @@ export class CalendarService {
|
|||||||
async updateSource(id: string, userId: string, dto: UpdateCalendarSourceDto) {
|
async updateSource(id: string, userId: string, dto: UpdateCalendarSourceDto) {
|
||||||
const existing = await this.prisma.calendarSource.findUnique({
|
const existing = await this.prisma.calendarSource.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
select: { userId: true },
|
select: { userId: true, type: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
@@ -184,7 +185,9 @@ export class CalendarService {
|
|||||||
throw new ForbiddenException('Not your calendar source');
|
throw new ForbiddenException('Not your calendar source');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dto.url) {
|
const effectiveType = dto.type ?? existing.type;
|
||||||
|
// Exchange EWS servers are commonly on internal networks — SSRF check skipped for exchange type
|
||||||
|
if (dto.url && effectiveType !== 'exchange') {
|
||||||
await this.validateUrlNotPrivate(dto.url);
|
await this.validateUrlNotPrivate(dto.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,7 +290,12 @@ export class CalendarService {
|
|||||||
async testConnectionFromConfig(
|
async testConnectionFromConfig(
|
||||||
dto: TestCalendarSourceConfigDto,
|
dto: TestCalendarSourceConfigDto,
|
||||||
): Promise<{ success: boolean; error?: string }> {
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
await this.validateUrlNotPrivate(dto.url);
|
try {
|
||||||
|
// Exchange EWS servers are commonly on internal networks — SSRF check skipped for exchange type
|
||||||
|
if (dto.type !== 'exchange') await this.validateUrlNotPrivate(dto.url);
|
||||||
|
} catch (e: any) {
|
||||||
|
return { success: false, error: e?.message ?? 'URL not allowed' };
|
||||||
|
}
|
||||||
|
|
||||||
const provider = this.getProvider(dto.type);
|
const provider = this.getProvider(dto.type);
|
||||||
const tempSource = {
|
const tempSource = {
|
||||||
@@ -302,7 +310,7 @@ export class CalendarService {
|
|||||||
try {
|
try {
|
||||||
const success = await provider.testConnection(tempSource);
|
const success = await provider.testConnection(tempSource);
|
||||||
return { success };
|
return { success };
|
||||||
} catch {
|
} catch (e: any) {
|
||||||
return { success: false, error: 'Connection failed' };
|
return { success: false, error: 'Connection failed' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ export function CalendarSettingsPanel() {
|
|||||||
const [editingId, setEditingId] = useState<string | null>(null);
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [saveError, setSaveError] = useState<string | null>(null);
|
||||||
|
const [editSaveError, setEditSaveError] = useState<string | null>(null);
|
||||||
const [testResults, setTestResults] = useState<Record<string, 'success' | 'error' | 'testing'>>({});
|
const [testResults, setTestResults] = useState<Record<string, 'success' | 'error' | 'testing'>>({});
|
||||||
|
|
||||||
// Load sources on mount
|
// Load sources on mount
|
||||||
@@ -76,6 +78,7 @@ export function CalendarSettingsPanel() {
|
|||||||
// Add new source
|
// Add new source
|
||||||
const handleAddSource = useCallback(async (payload: CreateSourcePayload) => {
|
const handleAddSource = useCallback(async (payload: CreateSourcePayload) => {
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
|
setSaveError(null);
|
||||||
try {
|
try {
|
||||||
const created = await addSource(payload);
|
const created = await addSource(payload);
|
||||||
setSources((prev) => [...prev, created]);
|
setSources((prev) => [...prev, created]);
|
||||||
@@ -93,11 +96,11 @@ export function CalendarSettingsPanel() {
|
|||||||
setTestResults((prev) => ({ ...prev, [created.id]: 'error' }));
|
setTestResults((prev) => ({ ...prev, [created.id]: 'error' }));
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Error handled silently
|
setSaveError(t('calendar.saveError') || 'Fehler beim Speichern');
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [t]);
|
||||||
|
|
||||||
// Delete source
|
// Delete source
|
||||||
const handleDeleteSource = useCallback(async (id: string) => {
|
const handleDeleteSource = useCallback(async (id: string) => {
|
||||||
@@ -282,6 +285,9 @@ export function CalendarSettingsPanel() {
|
|||||||
{/* Edit form (below the source being edited) */}
|
{/* Edit form (below the source being edited) */}
|
||||||
{editingId && (
|
{editingId && (
|
||||||
<div className="rounded-md border border-border bg-card p-4">
|
<div className="rounded-md border border-border bg-card p-4">
|
||||||
|
{editSaveError && (
|
||||||
|
<p className="mb-3 text-sm text-destructive">{editSaveError}</p>
|
||||||
|
)}
|
||||||
<h3 className="mb-3 text-sm font-semibold text-foreground">
|
<h3 className="mb-3 text-sm font-semibold text-foreground">
|
||||||
Edit Source
|
Edit Source
|
||||||
</h3>
|
</h3>
|
||||||
@@ -296,12 +302,14 @@ export function CalendarSettingsPanel() {
|
|||||||
url: s.url,
|
url: s.url,
|
||||||
username: s.username ?? '',
|
username: s.username ?? '',
|
||||||
exchangeMode: s.exchangeMode ?? undefined,
|
exchangeMode: s.exchangeMode ?? undefined,
|
||||||
|
domain: s.domain ?? undefined,
|
||||||
color: s.color ?? undefined,
|
color: s.color ?? undefined,
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
})()}
|
})()}
|
||||||
onSave={async (payload) => {
|
onSave={async (payload) => {
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
|
setEditSaveError(null);
|
||||||
try {
|
try {
|
||||||
const updated = await updateSource(editingId, payload);
|
const updated = await updateSource(editingId, payload);
|
||||||
setSources((prev) =>
|
setSources((prev) =>
|
||||||
@@ -309,7 +317,7 @@ export function CalendarSettingsPanel() {
|
|||||||
);
|
);
|
||||||
setEditingId(null);
|
setEditingId(null);
|
||||||
} catch {
|
} catch {
|
||||||
// Error handled silently
|
setEditSaveError(t('calendar.saveError') || 'Fehler beim Speichern');
|
||||||
} finally {
|
} finally {
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
}
|
}
|
||||||
@@ -337,6 +345,9 @@ export function CalendarSettingsPanel() {
|
|||||||
<h3 className="mb-3 text-sm font-semibold text-foreground">
|
<h3 className="mb-3 text-sm font-semibold text-foreground">
|
||||||
{t('calendar.addSource')}
|
{t('calendar.addSource')}
|
||||||
</h3>
|
</h3>
|
||||||
|
{saveError && (
|
||||||
|
<p className="mb-3 text-sm text-destructive">{saveError}</p>
|
||||||
|
)}
|
||||||
<CalendarSourceForm
|
<CalendarSourceForm
|
||||||
onSave={handleAddSource}
|
onSave={handleAddSource}
|
||||||
onCancel={() => setShowAddForm(false)}
|
onCancel={() => setShowAddForm(false)}
|
||||||
|
|||||||
@@ -208,6 +208,7 @@
|
|||||||
"formTesting": "Teste...",
|
"formTesting": "Teste...",
|
||||||
"formTestSuccess": "Verbindung erfolgreich",
|
"formTestSuccess": "Verbindung erfolgreich",
|
||||||
"formTestFailed": "Verbindung fehlgeschlagen",
|
"formTestFailed": "Verbindung fehlgeschlagen",
|
||||||
|
"saveError": "Speichern fehlgeschlagen. Bitte Eingaben prüfen.",
|
||||||
"formSave": "Speichern",
|
"formSave": "Speichern",
|
||||||
"formSaving": "Wird gespeichert...",
|
"formSaving": "Wird gespeichert...",
|
||||||
"formCancel": "Abbrechen",
|
"formCancel": "Abbrechen",
|
||||||
|
|||||||
@@ -208,6 +208,7 @@
|
|||||||
"formTesting": "Testing...",
|
"formTesting": "Testing...",
|
||||||
"formTestSuccess": "Connection successful",
|
"formTestSuccess": "Connection successful",
|
||||||
"formTestFailed": "Connection failed",
|
"formTestFailed": "Connection failed",
|
||||||
|
"saveError": "Save failed. Please check your input.",
|
||||||
"formSave": "Save",
|
"formSave": "Save",
|
||||||
"formSaving": "Saving...",
|
"formSaving": "Saving...",
|
||||||
"formCancel": "Cancel",
|
"formCancel": "Cancel",
|
||||||
|
|||||||
Reference in New Issue
Block a user