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).
|
||||
*/
|
||||
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> = {
|
||||
userId,
|
||||
@@ -174,7 +175,7 @@ export class CalendarService {
|
||||
async updateSource(id: string, userId: string, dto: UpdateCalendarSourceDto) {
|
||||
const existing = await this.prisma.calendarSource.findUnique({
|
||||
where: { id },
|
||||
select: { userId: true },
|
||||
select: { userId: true, type: true },
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
@@ -184,7 +185,9 @@ export class CalendarService {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -287,7 +290,12 @@ export class CalendarService {
|
||||
async testConnectionFromConfig(
|
||||
dto: TestCalendarSourceConfigDto,
|
||||
): 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 tempSource = {
|
||||
@@ -302,7 +310,7 @@ export class CalendarService {
|
||||
try {
|
||||
const success = await provider.testConnection(tempSource);
|
||||
return { success };
|
||||
} catch {
|
||||
} catch (e: any) {
|
||||
return { success: false, error: 'Connection failed' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ export function CalendarSettingsPanel() {
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||
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'>>({});
|
||||
|
||||
// Load sources on mount
|
||||
@@ -76,6 +78,7 @@ export function CalendarSettingsPanel() {
|
||||
// Add new source
|
||||
const handleAddSource = useCallback(async (payload: CreateSourcePayload) => {
|
||||
setIsSaving(true);
|
||||
setSaveError(null);
|
||||
try {
|
||||
const created = await addSource(payload);
|
||||
setSources((prev) => [...prev, created]);
|
||||
@@ -93,11 +96,11 @@ export function CalendarSettingsPanel() {
|
||||
setTestResults((prev) => ({ ...prev, [created.id]: 'error' }));
|
||||
}
|
||||
} catch {
|
||||
// Error handled silently
|
||||
setSaveError(t('calendar.saveError') || 'Fehler beim Speichern');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
// Delete source
|
||||
const handleDeleteSource = useCallback(async (id: string) => {
|
||||
@@ -282,6 +285,9 @@ export function CalendarSettingsPanel() {
|
||||
{/* Edit form (below the source being edited) */}
|
||||
{editingId && (
|
||||
<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">
|
||||
Edit Source
|
||||
</h3>
|
||||
@@ -296,12 +302,14 @@ export function CalendarSettingsPanel() {
|
||||
url: s.url,
|
||||
username: s.username ?? '',
|
||||
exchangeMode: s.exchangeMode ?? undefined,
|
||||
domain: s.domain ?? undefined,
|
||||
color: s.color ?? undefined,
|
||||
}
|
||||
: undefined;
|
||||
})()}
|
||||
onSave={async (payload) => {
|
||||
setIsSaving(true);
|
||||
setEditSaveError(null);
|
||||
try {
|
||||
const updated = await updateSource(editingId, payload);
|
||||
setSources((prev) =>
|
||||
@@ -309,7 +317,7 @@ export function CalendarSettingsPanel() {
|
||||
);
|
||||
setEditingId(null);
|
||||
} catch {
|
||||
// Error handled silently
|
||||
setEditSaveError(t('calendar.saveError') || 'Fehler beim Speichern');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
@@ -337,6 +345,9 @@ export function CalendarSettingsPanel() {
|
||||
<h3 className="mb-3 text-sm font-semibold text-foreground">
|
||||
{t('calendar.addSource')}
|
||||
</h3>
|
||||
{saveError && (
|
||||
<p className="mb-3 text-sm text-destructive">{saveError}</p>
|
||||
)}
|
||||
<CalendarSourceForm
|
||||
onSave={handleAddSource}
|
||||
onCancel={() => setShowAddForm(false)}
|
||||
|
||||
@@ -208,6 +208,7 @@
|
||||
"formTesting": "Teste...",
|
||||
"formTestSuccess": "Verbindung erfolgreich",
|
||||
"formTestFailed": "Verbindung fehlgeschlagen",
|
||||
"saveError": "Speichern fehlgeschlagen. Bitte Eingaben prüfen.",
|
||||
"formSave": "Speichern",
|
||||
"formSaving": "Wird gespeichert...",
|
||||
"formCancel": "Abbrechen",
|
||||
|
||||
@@ -208,6 +208,7 @@
|
||||
"formTesting": "Testing...",
|
||||
"formTestSuccess": "Connection successful",
|
||||
"formTestFailed": "Connection failed",
|
||||
"saveError": "Save failed. Please check your input.",
|
||||
"formSave": "Save",
|
||||
"formSaving": "Saving...",
|
||||
"formCancel": "Cancel",
|
||||
|
||||
Reference in New Issue
Block a user