mirror of
https://github.com/nfonteyne/octane-website.git
synced 2026-09-03 23:24:48 +02:00
update: work with webcal
This commit is contained in:
parent
5dce6eb89b
commit
fd81b74594
4 changed files with 40 additions and 3 deletions
12
src/lib/icsUrl.js
Normal file
12
src/lib/icsUrl.js
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Apple/iCloud (and some other providers) hand out webcal:// links — that
|
||||||
|
// scheme is just a convention meaning "this is a calendar subscription, open
|
||||||
|
// it with your calendar app" and isn't something fetch() can request, so
|
||||||
|
// without this it fails with a confusing network error. webcal(s):// always
|
||||||
|
// resolves to the same feed over plain http(s), so swap the scheme and fetch
|
||||||
|
// that instead. Applied once, at add-time, so the stored URL is already the
|
||||||
|
// fetchable form and every later sync just works.
|
||||||
|
function normalizeIcsUrl(url) {
|
||||||
|
return url.replace(/^webcals?:\/\//i, 'https://');
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { normalizeIcsUrl };
|
||||||
|
|
@ -3,6 +3,7 @@ const calendarRepo = require('../repositories/calendarRepo');
|
||||||
const usersRepo = require('../repositories/usersRepo');
|
const usersRepo = require('../repositories/usersRepo');
|
||||||
const workflowState = require('../lib/calendarWorkflowState');
|
const workflowState = require('../lib/calendarWorkflowState');
|
||||||
const calendarSync = require('../services/calendarSync');
|
const calendarSync = require('../services/calendarSync');
|
||||||
|
const { normalizeIcsUrl } = require('../lib/icsUrl');
|
||||||
const { requireAdmin } = require('../auth/middleware');
|
const { requireAdmin } = require('../auth/middleware');
|
||||||
const asyncHandler = require('../lib/asyncHandler');
|
const asyncHandler = require('../lib/asyncHandler');
|
||||||
|
|
||||||
|
|
@ -93,7 +94,7 @@ router.post(
|
||||||
if (!icsUrl || !icsUrl.trim()) {
|
if (!icsUrl || !icsUrl.trim()) {
|
||||||
return res.status(400).json({ error: 'ics_url_required' });
|
return res.status(400).json({ error: 'ics_url_required' });
|
||||||
}
|
}
|
||||||
const trimmedUrl = icsUrl.trim();
|
const trimmedUrl = normalizeIcsUrl(icsUrl.trim());
|
||||||
try {
|
try {
|
||||||
await calendarSync.testFeed(trimmedUrl);
|
await calendarSync.testFeed(trimmedUrl);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -148,7 +149,7 @@ router.post(
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(404).json({ error: 'user_not_found' });
|
return res.status(404).json({ error: 'user_not_found' });
|
||||||
}
|
}
|
||||||
const trimmedUrl = icsUrl.trim();
|
const trimmedUrl = normalizeIcsUrl(icsUrl.trim());
|
||||||
try {
|
try {
|
||||||
await calendarSync.testFeed(trimmedUrl);
|
await calendarSync.testFeed(trimmedUrl);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ const ical = require('node-ical');
|
||||||
const calendarRepo = require('../repositories/calendarRepo');
|
const calendarRepo = require('../repositories/calendarRepo');
|
||||||
const { generateSlots, isBusyDuring, widenWindow } = require('../lib/calendarAvailability');
|
const { generateSlots, isBusyDuring, widenWindow } = require('../lib/calendarAvailability');
|
||||||
const { parisWallClockToUTC } = require('../lib/calendarDates');
|
const { parisWallClockToUTC } = require('../lib/calendarDates');
|
||||||
|
const { normalizeIcsUrl } = require('../lib/icsUrl');
|
||||||
|
|
||||||
const RANGE_DAYS = 105; // slightly more than the 14 weeks generateSlots() covers
|
const RANGE_DAYS = 105; // slightly more than the 14 weeks generateSlots() covers
|
||||||
const FETCH_TIMEOUT_MS = 15000;
|
const FETCH_TIMEOUT_MS = 15000;
|
||||||
|
|
@ -13,7 +14,10 @@ async function fetchIcsCalendar(icsUrl) {
|
||||||
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
||||||
let text;
|
let text;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(icsUrl, { signal: controller.signal });
|
// Defensive: normalize here too, not just at add-time in routes/calendar.js,
|
||||||
|
// so a webcal:// URL stored before that fix (or inserted directly) doesn't
|
||||||
|
// keep silently failing every sync.
|
||||||
|
const response = await fetch(normalizeIcsUrl(icsUrl), { signal: controller.signal });
|
||||||
if (!response.ok) throw new Error(`ICS fetch failed: ${response.status}`);
|
if (!response.ok) throw new Error(`ICS fetch failed: ${response.status}`);
|
||||||
text = await response.text();
|
text = await response.text();
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
20
test/icsUrl.test.js
Normal file
20
test/icsUrl.test.js
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
const { test } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { normalizeIcsUrl } = require('../src/lib/icsUrl');
|
||||||
|
|
||||||
|
test('normalizeIcsUrl: rewrites webcal:// to https://', () => {
|
||||||
|
assert.equal(normalizeIcsUrl('webcal://p01-caldav.icloud.com/foo.ics'), 'https://p01-caldav.icloud.com/foo.ics');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('normalizeIcsUrl: rewrites webcals:// to https://', () => {
|
||||||
|
assert.equal(normalizeIcsUrl('webcals://p01-caldav.icloud.com/foo.ics'), 'https://p01-caldav.icloud.com/foo.ics');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('normalizeIcsUrl: is case-insensitive on the scheme', () => {
|
||||||
|
assert.equal(normalizeIcsUrl('WebCal://example.com/foo.ics'), 'https://example.com/foo.ics');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('normalizeIcsUrl: leaves http/https URLs untouched', () => {
|
||||||
|
assert.equal(normalizeIcsUrl('https://example.com/foo.ics'), 'https://example.com/foo.ics');
|
||||||
|
assert.equal(normalizeIcsUrl('http://example.com/foo.ics'), 'http://example.com/foo.ics');
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue